Shortcut rework / Configure shortcuts on the vpk command line (#165)

This commit is contained in:
Caelan
2024-07-14 15:33:31 +01:00
committed by GitHub
parent fc11832376
commit b9f7c707f5
27 changed files with 1383 additions and 724 deletions

550
src/Rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,14 +52,14 @@ regex = "1.10"
rand = "0.8" rand = "0.8"
log = "0.4" log = "0.4"
simplelog = "0.12" simplelog = "0.12"
clap = "4.4" clap = "4.5"
xml = "0.8" xml = "0.8"
semver = "1.0" semver = "1.0"
chrono = "0.4" chrono = "0.4"
wait-timeout = "0.2" wait-timeout = "0.2"
lazy_static = "1.4" lazy_static = "1.5"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
ureq = { version = "2.9", default-features = false, features = [ ureq = { version = "2.10", default-features = false, features = [
"native-tls", "native-tls",
"gzip", "gzip",
] } ] }
@@ -79,6 +79,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
time = "0.3" time = "0.3"
os_info = "3.8" os_info = "3.8"
bitflags = "2.6"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
native-dialog = "0.7" native-dialog = "0.7"
@@ -87,15 +88,15 @@ dialog = "0.3"
libc = "0.2" libc = "0.2"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
fs_extra = "1.2" fs_extra = "1.3"
memmap2 = "0.9" memmap2 = "0.9"
winsafe = { version = "0.0.20", features = ["version", "user", "gui"] } winsafe = { version = "0.0.20", features = ["gui"] }
image = { version = "0.25", default-features = false, features = [ image = { version = "0.25", default-features = false, features = [
"gif", "gif",
"jpeg", "jpeg",
"png", "png",
] } ] }
windows = { version = "0.56", default-features = false, features = [ windows = { version = "0.57", default-features = false, features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_Security", "Win32_Security",
"Win32_System_Com", "Win32_System_Com",
@@ -104,6 +105,7 @@ windows = { version = "0.56", default-features = false, features = [
"Win32_System_Threading", "Win32_System_Threading",
"Win32_System_SystemInformation", "Win32_System_SystemInformation",
"Win32_System_Variant", "Win32_System_Variant",
"Win32_System_Environment",
"Win32_Storage_EnhancedStorage", "Win32_Storage_EnhancedStorage",
"Win32_Storage_FileSystem", "Win32_Storage_FileSystem",
"Win32_System_Com_StructuredStorage", "Win32_System_Com_StructuredStorage",
@@ -111,24 +113,20 @@ windows = { version = "0.56", default-features = false, features = [
"Win32_System_Threading", "Win32_System_Threading",
"Win32_System_ProcessStatus", "Win32_System_ProcessStatus",
"Win32_System_WindowsProgramming", "Win32_System_WindowsProgramming",
"Win32_System_LibraryLoader",
"Win32_UI_Shell_Common", "Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem", "Win32_UI_Shell_PropertiesSystem",
] } "Win32_UI_WindowsAndMessaging",
windows-sys = { version = "0.52", default-features = false, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage",
"Win32_Storage_FileSystem",
"Win32_System_Kernel", "Win32_System_Kernel",
"Win32_System_Threading",
"Win32_System_WindowsProgramming",
"Wdk", "Wdk",
"Wdk_System", "Wdk_System",
"Wdk_System_Threading", "Wdk_System_Threading",
] } ] }
normpath = "1.0.1" normpath = "1.2"
webview2-com = "0.30" webview2-com = "0.31"
libloading = "0.8" libloading = "0.8"
strsim = "0.11"
same-file = "1.0"
[dev-dependencies] [dev-dependencies]
tempfile = "3.9" tempfile = "3.9"

View File

@@ -1,4 +1,4 @@
max_width = 160 max_width = 140
use_small_heuristics = "Max" use_small_heuristics = "Max"
indent_style = "Visual" indent_style = "Visual"
unstable_features = true unstable_features = true

View File

@@ -16,7 +16,15 @@ fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<
let dest = dest.as_ref(); let dest = dest.as_ref();
// robocopy C:\source\something.new C:\destination\something /MIR /ZB /W:5 /R:5 /MT:8 /LOG:C:\logs\copy_log.txt // robocopy C:\source\something.new C:\destination\something /MIR /ZB /W:5 /R:5 /MT:8 /LOG:C:\logs\copy_log.txt
let cmd = std::process::Command::new("robocopy").arg(source).arg(dest).arg("/MIR").arg("/IS").arg("/W:1").arg("/R:5").arg("/MT:2").output()?; let cmd = std::process::Command::new("robocopy")
.arg(source)
.arg(dest)
.arg("/MIR")
.arg("/IS")
.arg("/W:1")
.arg("/R:5")
.arg("/MT:2")
.output()?;
let stdout = String::from_utf8_lossy(&cmd.stdout); let stdout = String::from_utf8_lossy(&cmd.stdout);
let stderr = String::from_utf8_lossy(&cmd.stderr); let stderr = String::from_utf8_lossy(&cmd.stderr);
@@ -33,28 +41,28 @@ fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<
Ok(()) Ok(())
} }
pub fn apply_package_impl<'a>(root_path: &PathBuf, app: &Manifest, package: &PathBuf, runhooks: bool) -> Result<Manifest> { pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &PathBuf, run_hooks: bool) -> Result<Manifest> {
let bundle = bundle::load_bundle_from_file(package)?; let bundle = bundle::load_bundle_from_file(package)?;
let manifest = bundle.read_manifest()?; let new_app = bundle.read_manifest()?;
let found_version = (manifest.version).to_owned(); let found_version = (new_app.version).to_owned();
info!("Applying package to current: {}", found_version); info!("Applying package to current: {} (old version {})", found_version, old_app.version);
if !crate::windows::prerequisite::prompt_and_install_all_missing(&manifest, Some(&app.version))? { if !crate::windows::prerequisite::prompt_and_install_all_missing(&new_app, Some(&old_app.version))? {
bail!("Stopping apply. Pre-requisites are missing and user cancelled."); bail!("Stopping apply. Pre-requisites are missing and user cancelled.");
} }
let packages_dir = app.get_packages_path(root_path); let packages_dir = old_app.get_packages_path(root_path);
let packages_dir = Path::new(&packages_dir); let packages_dir = Path::new(&packages_dir);
let current_dir = app.get_current_path(root_path); let current_dir = old_app.get_current_path(root_path);
let temp_path_new = packages_dir.join(format!("tmp_{}", shared::random_string(16))); let temp_path_new = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
let temp_path_old = packages_dir.join(format!("tmp_{}", shared::random_string(16))); let temp_path_old = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
// open a dialog showing progress... // open a dialog showing progress...
let (mut tx, _) = mpsc::channel::<i16>(); let (mut tx, _) = mpsc::channel::<i16>();
if !dialogs::get_silent() { if !dialogs::get_silent() {
let title = format!("{} Update", &manifest.title); let title = format!("{} Update", &new_app.title);
let message = format!("Installing update {}...", &manifest.version); let message = format!("Installing update {}...", &new_app.version);
tx = splash::show_progress_dialog(title, message); tx = splash::show_progress_dialog(title, message);
} }
@@ -68,15 +76,15 @@ pub fn apply_package_impl<'a>(root_path: &PathBuf, app: &Manifest, package: &Pat
let _ = tx.send(splash::MSG_INDEFINITE); let _ = tx.send(splash::MSG_INDEFINITE);
// second, run application hooks (but don't care if it fails) // second, run application hooks (but don't care if it fails)
if runhooks { if run_hooks {
crate::windows::run_hook(app, root_path, "--veloapp-obsolete", 15); crate::windows::run_hook(old_app, root_path, "--veloapp-obsolete", 15);
} else { } else {
info!("Skipping --veloapp-obsolete hook."); info!("Skipping --veloapp-obsolete hook.");
} }
// third, we try _REALLY HARD_ to stop the package // third, we try _REALLY HARD_ to stop the package
let _ = shared::force_stop_package(root_path); let _ = shared::force_stop_package(root_path);
if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&app.title, &current_dir) { if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&old_app.title, &current_dir) {
bail!("Failed to close processes locking directory / user cancelled."); bail!("Failed to close processes locking directory / user cancelled.");
} }
@@ -108,9 +116,10 @@ pub fn apply_package_impl<'a>(root_path: &PathBuf, app: &Manifest, package: &Pat
let _ = tx.send(splash::MSG_CLOSE); let _ = tx.send(splash::MSG_CLOSE);
info!("Showing error dialog..."); info!("Showing error dialog...");
let title = format!("{} Update", &manifest.title); let title = format!("{} Update", &new_app.title);
let header = "Failed to update"; let header = "Failed to update";
let body = format!("Failed to update {} to version {}. Please check the logs for more details.", &manifest.title, &manifest.version); let body =
format!("Failed to update {} to version {}. Please check the logs for more details.", &new_app.title, &new_app.version);
dialogs::show_error(&title, Some(header), &body); dialogs::show_error(&title, Some(header), &body);
bail!("Fatal error performing update."); bail!("Fatal error performing update.");
@@ -119,17 +128,25 @@ pub fn apply_package_impl<'a>(root_path: &PathBuf, app: &Manifest, package: &Pat
// from this point on, we're past the point of no return and should not bail // from this point on, we're past the point of no return and should not bail
// sixth, we write the uninstall entry // sixth, we write the uninstall entry
if let Err(e) = manifest.write_uninstall_entry(root_path) { if let Err(e) = new_app.write_uninstall_entry(root_path) {
warn!("Failed to write uninstall entry ({}).", e); warn!("Failed to write uninstall entry ({}).", e);
} }
// seventh, we run the post-install hooks // seventh, we run the post-install hooks
if runhooks { if run_hooks {
crate::windows::run_hook(&manifest, &root_path, "--veloapp-updated", 15); crate::windows::run_hook(&new_app, &root_path, "--veloapp-updated", 15);
} else { } else {
info!("Skipping --veloapp-updated hook."); info!("Skipping --veloapp-updated hook.");
} }
// update application shortcuts
// should try and remove the temp dirs before recalculating the shortcuts,
// because windows may try to use the "Distributed Link Tracking and Object Identifiers (DLT) service"
// to update the shortcut to point at the temp/renamed location
let _ = remove_dir_all::remove_dir_all(&temp_path_new);
let _ = remove_dir_all::remove_dir_all(&temp_path_old);
crate::windows::create_or_update_manifest_lnks(root_path, &new_app, Some(old_app));
// done! // done!
info!("Package applied successfully."); info!("Package applied successfully.");
Ok(()) Ok(())
@@ -139,5 +156,5 @@ pub fn apply_package_impl<'a>(root_path: &PathBuf, app: &Manifest, package: &Pat
let _ = remove_dir_all::remove_dir_all(&temp_path_new); let _ = remove_dir_all::remove_dir_all(&temp_path_new);
let _ = remove_dir_all::remove_dir_all(&temp_path_old); let _ = remove_dir_all::remove_dir_all(&temp_path_old);
action?; action?;
Ok(manifest) Ok(new_app)
} }

View File

@@ -3,6 +3,8 @@ use crate::{
shared::{self, bundle, runtime_arch::RuntimeArch}, shared::{self, bundle, runtime_arch::RuntimeArch},
windows, windows,
}; };
use ::windows::core::PCWSTR;
use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use memmap2::Mmap; use memmap2::Mmap;
use pretty_bytes_rust::pretty_bytes; use pretty_bytes_rust::pretty_bytes;
@@ -11,15 +13,14 @@ use std::{
fs::{self, File}, fs::{self, File},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use winsafe::{self as w, co};
pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Result<()> { pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Result<()> {
let osinfo = os_info::get(); let osinfo = os_info::get();
let osarch = RuntimeArch::from_current_system(); let osarch = RuntimeArch::from_current_system();
info!("OS: {osinfo}, Arch={osarch:#?}"); info!("OS: {osinfo}, Arch={osarch:#?}");
if !w::IsWindows7OrGreater()? { if !windows::is_windows_7_sp1_or_greater() {
bail!("This installer requires Windows 7 or later and cannot run."); bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
} }
let file = File::open(env::current_exe()?)?; let file = File::open(env::current_exe()?)?;
@@ -51,7 +52,7 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
let (root_path, root_is_default) = if install_to.is_some() { let (root_path, root_is_default) = if install_to.is_some() {
(install_to.unwrap().clone(), false) (install_to.unwrap().clone(), false)
} else { } else {
let appdata = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::LocalAppData, co::KF::DONT_UNEXPAND, None)?; let appdata = windows::known_path::get_local_app_data()?;
(Path::new(&appdata).join(&app.id), true) (Path::new(&appdata).join(&app.id), true)
}; };
@@ -66,18 +67,26 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
// do we have enough disk space? // do we have enough disk space?
let (compressed_size, extracted_size) = pkg.calculate_size(); let (compressed_size, extracted_size) = pkg.calculate_size();
let required_space = compressed_size + extracted_size + (50 * 1000 * 1000); // archive + velopack overhead let required_space = compressed_size + extracted_size + (50 * 1000 * 1000); // archive + velopack overhead
let mut free_space: u64 = 0; let mut free_space: u64 = 0;
w::GetDiskFreeSpaceEx(Some(&root_path_str), None, None, Some(&mut free_space))?; let root_pcwstr = windows::strings::string_to_u16(root_path_str);
if free_space < required_space { let root_pcwstr: PCWSTR = PCWSTR(root_pcwstr.as_ptr());
bail!( if let Ok(()) = unsafe { GetDiskFreeSpaceExW(root_pcwstr, None, None, Some(&mut free_space)) } {
"{} requires at least {} disk space to be installed. There is only {} available.", if free_space < required_space {
&app.title, bail!(
pretty_bytes(required_space, None), "{} requires at least {} disk space to be installed. There is only {} available.",
pretty_bytes(free_space, None) &app.title,
); pretty_bytes(required_space, None),
pretty_bytes(free_space, None)
);
}
} }
info!("There is {} free space available at destination, this package requires {}.", pretty_bytes(free_space, None), pretty_bytes(required_space, None)); info!(
"There is {} free space available at destination, this package requires {}.",
pretty_bytes(free_space, None),
pretty_bytes(required_space, None)
);
// does this app support this OS / architecture? // does this app support this OS / architecture?
if !app.os_min_version.is_empty() && !windows::is_os_version_or_greater(&app.os_min_version)? { if !app.os_min_version.is_empty() && !windows::is_os_version_or_greater(&app.os_min_version)? {
@@ -99,8 +108,9 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
} }
info!("User chose to overwrite existing installation."); info!("User chose to overwrite existing installation.");
shared::force_stop_package(&root_path) shared::force_stop_package(&root_path).map_err(|z| {
.map_err(|z| anyhow!("Failed to stop application ({}), please close the application and try running the installer again.", z))?; anyhow!("Failed to stop application ({}), please close the application and try running the installer again.", z)
})?;
root_path_renamed = format!("{}_{}", root_path_str, shared::random_string(8)); root_path_renamed = format!("{}_{}", root_path_str, shared::random_string(8));
info!("Renaming existing directory to '{}' to allow rollback...", root_path_renamed); info!("Renaming existing directory to '{}' to allow rollback...", root_path_renamed);
@@ -180,13 +190,17 @@ fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::m
bail!("The main executable could not be found in the package. Please contact the application author."); bail!("The main executable could not be found in the package. Please contact the application author.");
} }
info!("Creating new default shortcuts..."); info!("Creating shortcuts...");
let _ = windows::create_default_lnks(&root_path, &app); windows::create_or_update_manifest_lnks(&root_path, &app, None);
info!("Starting process install hook"); info!("Starting process install hook");
if windows::run_hook(&app, &root_path, "--veloapp-install", 30) == false { if !windows::run_hook(&app, &root_path, "--veloapp-install", 30) {
let setup_name = format!("{} Setup {}", app.title, app.version); let setup_name = format!("{} Setup {}", app.title, app.version);
dialogs::show_warn(&setup_name, None, "Installation has completed, but the application install hook failed. It may not have installed correctly."); dialogs::show_warn(
&setup_name,
None,
"Installation has completed, but the application install hook failed. It may not have installed correctly.",
);
} }
let _ = tx.send(100); let _ = tx.send(100);

View File

@@ -13,7 +13,13 @@ use std::{
}; };
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
pub fn start_impl(root_dir: &PathBuf, app: &Manifest, exe_name: Option<&String>, exe_args: Option<Vec<&str>>, legacy_args: Option<&String>) -> Result<()> { pub fn start_impl(
root_dir: &PathBuf,
app: &Manifest,
exe_name: Option<&String>,
exe_args: Option<Vec<&str>>,
legacy_args: Option<&String>,
) -> Result<()> {
match shared::has_app_prefixed_folder(root_dir) { match shared::has_app_prefixed_folder(root_dir) {
Ok(has_prefix) => { Ok(has_prefix) => {
if has_prefix { if has_prefix {
@@ -86,21 +92,19 @@ fn try_legacy_migration(root_dir: &PathBuf, app: &bundle::Manifest) -> Result<()
} }
} }
info!("Removing old shortcuts...");
win::remove_all_shortcuts_for_root_dir(root_dir);
let mut modified_app = app.clone();
modified_app.shortcut_locations = "".to_string(); // reset, so we install new shortcuts
info!("Applying latest full package..."); info!("Applying latest full package...");
let buf = Path::new(&package.file_path).to_path_buf(); let buf = Path::new(&package.file_path).to_path_buf();
super::apply(root_dir, app, false, OperationWait::NoWait, Some(&buf), None, false)?; super::apply(root_dir, &modified_app, false, OperationWait::NoWait, Some(&buf), None, false)?;
info!("Removing old app-* folders..."); info!("Removing old app-* folders...");
shared::delete_app_prefixed_folders(root_dir)?; shared::delete_app_prefixed_folders(root_dir)?;
let _ = remove_dir_all::remove_dir_all(root_dir.join("staging")); let _ = remove_dir_all::remove_dir_all(root_dir.join("staging"));
info!("Removing old shortcuts...");
if let Err(e) = win::remove_all_shortcuts_for_root_dir(root_dir) {
warn!("Failed to remove shortcuts ({}).", e);
}
info!("Creating new default shortcuts...");
let _ = win::create_default_lnks(root_dir, app);
Ok(()) Ok(())
} }

View File

@@ -16,10 +16,8 @@ pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Resu
// run uninstall hook // run uninstall hook
windows::run_hook(&app, root_path, "--veloapp-uninstall", 60); windows::run_hook(&app, root_path, "--veloapp-uninstall", 60);
if let Err(e) = windows::remove_all_shortcuts_for_root_dir(&root_path) { // remove all shortcuts pointing to the app
error!("Unable to remove shortcuts ({}).", e); windows::remove_all_shortcuts_for_root_dir(&root_path);
// finished_with_errors = true;
}
info!("Removing directory '{}'", root_path.to_string_lossy()); info!("Removing directory '{}'", root_path.to_string_lossy());
if let Err(e) = shared::retry_io(|| remove_dir_all::remove_dir_but_not_self(&root_path)) { if let Err(e) = shared::retry_io(|| remove_dir_all::remove_dir_but_not_self(&root_path)) {

View File

@@ -373,7 +373,10 @@ impl BundleInfo<'_> {
for i in 0..archive.len() { for i in 0..archive.len() {
let file = archive.by_index(i)?; let file = archive.by_index(i)?;
let key = file.enclosed_name().ok_or_else(|| { let key = file.enclosed_name().ok_or_else(|| {
anyhow!("Could not extract file safely ({}). Ensure no paths in archive are absolute or point to a path outside the archive.", file.name()) anyhow!(
"Could not extract file safely ({}). Ensure no paths in archive are absolute or point to a path outside the archive.",
file.name()
)
})?; })?;
files.push(key.to_string_lossy().to_string()); files.push(key.to_string_lossy().to_string());
} }
@@ -396,6 +399,8 @@ pub struct Manifest {
pub os: String, pub os: String,
pub os_min_version: String, pub os_min_version: String,
pub channel: String, pub channel: String,
pub shortcut_locations: String,
pub shortcut_amuid: String,
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -436,7 +441,8 @@ impl Manifest {
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path); let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet = format!("\"{}\" --uninstall --silent", updater_path); let uninstall_quiet = format!("\"{}\" --uninstall --silent", updater_path);
let reg_uninstall = w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0; let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
let reg_app = reg_uninstall.RegCreateKeyEx(&self.id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0; let reg_app = reg_uninstall.RegCreateKeyEx(&self.id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?; reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(self.title.to_owned()))?; reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(self.title.to_owned()))?;
@@ -454,7 +460,8 @@ impl Manifest {
} }
pub fn remove_uninstall_entry(&self) -> Result<()> { pub fn remove_uninstall_entry(&self) -> Result<()> {
info!("Removing uninstall registry keys..."); info!("Removing uninstall registry keys...");
let reg_uninstall = w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0; let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
reg_uninstall.RegDeleteKey(&self.id)?; reg_uninstall.RegDeleteKey(&self.id)?;
Ok(()) Ok(())
} }
@@ -497,6 +504,10 @@ pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
obj.os_min_version = text; obj.os_min_version = text;
} else if el_name == "channel" { } else if el_name == "channel" {
obj.channel = text; obj.channel = text;
} else if el_name == "shortcutLocations" {
obj.shortcut_locations = text;
} else if el_name == "shortcutAmuid" {
obj.shortcut_amuid = text;
} }
} }
Ok(XmlEvent::EndElement { .. }) => { Ok(XmlEvent::EndElement { .. }) => {

View File

@@ -51,7 +51,7 @@ fn get_download_agent() -> Result<ureq::Agent> {
let mut tls_builder = native_tls::TlsConnector::builder(); let mut tls_builder = native_tls::TlsConnector::builder();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
if !winsafe::IsWindows10OrGreater()? { if !crate::windows::is_windows_10_or_greater() {
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled."); warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled."); warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled."); warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
@@ -65,7 +65,10 @@ fn get_download_agent() -> Result<ureq::Agent> {
#[test] #[test]
fn test_download_uses_tls_and_encoding_correctly() { fn test_download_uses_tls_and_encoding_correctly() {
assert_eq!(download_url_as_string("https://dotnetcli.blob.core.windows.net/dotnet/WindowsDesktop/5.0/latest.version").unwrap(), "5.0.17"); assert_eq!(
download_url_as_string("https://dotnetcli.blob.core.windows.net/dotnet/WindowsDesktop/5.0/latest.version").unwrap(),
"5.0.17"
);
} }
#[test] #[test]

View File

@@ -1,3 +1,5 @@
use ::windows::Win32::System::ProcessStatus::EnumProcesses;
use ::windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use regex::Regex; use regex::Regex;
use semver::Version; use semver::Version;
@@ -7,10 +9,8 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command as Process, process::Command as Process,
}; };
use windows::Win32::System::ProcessStatus::EnumProcesses; use windows::Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation};
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; use windows::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
use windows_sys::Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
use winsafe::{self as w, co, prelude::*}; use winsafe::{self as w, co, prelude::*};
use super::bundle::{self, EntryNameInfo, Manifest}; use super::bundle::{self, EntryNameInfo, Manifest};
@@ -35,7 +35,7 @@ pub fn wait_for_parent_to_exit(ms_to_wait: u32) -> Result<()> {
let mut info = PROCESS_BASIC_INFORMATION { let mut info = PROCESS_BASIC_INFORMATION {
AffinityMask: 0, AffinityMask: 0,
BasePriority: 0, BasePriority: 0,
ExitStatus: 0, ExitStatus: Default::default(),
InheritedFromUniqueProcessId: 0, InheritedFromUniqueProcessId: 0,
PebBaseAddress: std::ptr::null_mut(), PebBaseAddress: std::ptr::null_mut(),
UniqueProcessId: 0, UniqueProcessId: 0,
@@ -43,10 +43,9 @@ pub fn wait_for_parent_to_exit(ms_to_wait: u32) -> Result<()> {
let info_ptr: *mut ::core::ffi::c_void = &mut info as *mut _ as *mut ::core::ffi::c_void; let info_ptr: *mut ::core::ffi::c_void = &mut info as *mut _ as *mut ::core::ffi::c_void;
let info_size = std::mem::size_of::<PROCESS_BASIC_INFORMATION>() as u32; let info_size = std::mem::size_of::<PROCESS_BASIC_INFORMATION>() as u32;
let hr = unsafe { NtQueryInformationProcess(handle, basic_info, info_ptr, info_size, return_length_ptr) }; let hres = unsafe { NtQueryInformationProcess(handle, basic_info, info_ptr, info_size, return_length_ptr) };
if hres.is_err() {
if hr != 0 { return Err(anyhow!("Failed to query process information: {:?}", hres));
return Err(anyhow!("Failed to query process information: {}", hr));
} }
if info.InheritedFromUniqueProcessId <= 1 { if info.InheritedFromUniqueProcessId <= 1 {
@@ -346,7 +345,7 @@ fn get_all_packages(root_path: &PathBuf) -> Vec<EntryNameInfo> {
#[test] #[test]
fn test_get_running_processes_finds_cargo() { fn test_get_running_processes_finds_cargo() {
let profile = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Profile, co::KF::DONT_UNEXPAND, None).unwrap(); let profile = crate::windows::known_path::get_user_profile().unwrap();
let path = Path::new(&profile); let path = Path::new(&profile);
let rustup = path.join(".rustup"); let rustup = path.join(".rustup");

View File

@@ -0,0 +1,61 @@
use anyhow::Result;
use std::path::Path;
use windows::{
core::GUID,
Win32::UI::Shell::{
FOLDERID_Desktop, FOLDERID_Downloads, FOLDERID_LocalAppData, FOLDERID_Profile, FOLDERID_ProgramFilesX64, FOLDERID_ProgramFilesX86,
FOLDERID_RoamingAppData, FOLDERID_StartMenu, FOLDERID_Startup, SHGetKnownFolderPath,
},
};
fn get_known_folder(rfid: *const GUID) -> Result<String> {
unsafe {
let flag = windows::Win32::UI::Shell::KNOWN_FOLDER_FLAG(0);
let result = SHGetKnownFolderPath(rfid, flag, None)?;
super::strings::pwstr_to_string(result)
}
}
pub fn get_local_app_data() -> Result<String> {
get_known_folder(&FOLDERID_LocalAppData)
}
pub fn get_roaming_app_data() -> Result<String> {
get_known_folder(&FOLDERID_RoamingAppData)
}
pub fn get_user_desktop() -> Result<String> {
get_known_folder(&FOLDERID_Desktop)
}
pub fn get_user_profile() -> Result<String> {
get_known_folder(&FOLDERID_Profile)
}
pub fn get_start_menu() -> Result<String> {
let start_menu = get_known_folder(&FOLDERID_StartMenu)?;
let programs_path = Path::new(&start_menu).join("Programs");
Ok(programs_path.to_string_lossy().to_string())
}
pub fn get_startup() -> Result<String> {
get_known_folder(&FOLDERID_Startup)
}
pub fn get_downloads() -> Result<String> {
get_known_folder(&FOLDERID_Downloads)
}
pub fn get_program_files_x64() -> Result<String> {
get_known_folder(&FOLDERID_ProgramFilesX64)
}
pub fn get_program_files_x86() -> Result<String> {
get_known_folder(&FOLDERID_ProgramFilesX86)
}
pub fn get_user_pinned() -> Result<String> {
let pinned_str = get_roaming_app_data()?;
let pinned_path = Path::new(&pinned_str).join("Microsoft\\Internet Explorer\\Quick Launch\\User Pinned");
Ok(pinned_path.to_string_lossy().to_string())
}

View File

@@ -1,4 +1,4 @@
use windows_sys::Win32::System::LibraryLoader::{SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_SYSTEM32}; use windows::Win32::System::LibraryLoader::{SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_SYSTEM32};
/// This attempts to defend against malicious DLLs that may sit alongside /// This attempts to defend against malicious DLLs that may sit alongside
/// our binary in the user's download folder. /// our binary in the user's download folder.
#[cfg(windows)] #[cfg(windows)]
@@ -7,6 +7,6 @@ pub fn pre_main_sideload_mitigation() {
// For DLLs loaded at load time, this relies on the `delayload` linker flag. // For DLLs loaded at load time, this relies on the `delayload` linker flag.
// This is only necessary prior to Windows 10 RS1. See build.rs for details. // This is only necessary prior to Windows 10 RS1. See build.rs for details.
unsafe { unsafe {
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32); let _ = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32);
} }
} }

View File

@@ -3,6 +3,8 @@ pub mod mitigate;
pub mod prerequisite; pub mod prerequisite;
pub mod runtimes; pub mod runtimes;
pub mod splash; pub mod splash;
pub mod known_path;
pub mod strings;
mod self_delete; mod self_delete;
mod shortcuts; mod shortcuts;

View File

@@ -3,7 +3,6 @@ use crate::shared::{bundle, dialogs, download};
use anyhow::Result; use anyhow::Result;
use std::path::Path; use std::path::Path;
use winsafe::{self as w, co};
pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Option<&semver::Version>) -> Result<bool> { pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Option<&semver::Version>) -> Result<bool> {
info!("Checking application pre-requisites..."); info!("Checking application pre-requisites...");
@@ -38,7 +37,7 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt
} }
} }
let downloads = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Downloads, co::KF::DONT_UNEXPAND, None)?; let downloads = super::known_path::get_downloads()?;
let downloads = Path::new(downloads.as_str()); let downloads = Path::new(downloads.as_str());
info!("Downloading {} missing pre-requisites...", missing.len()); info!("Downloading {} missing pre-requisites...", missing.len());

View File

@@ -5,8 +5,8 @@ use anyhow::{anyhow, bail, Result};
use regex::Regex; use regex::Regex;
use std::process::Command as Process; use std::process::Command as Process;
use std::{collections::HashMap, fs, path::Path}; use std::{collections::HashMap, fs, path::Path};
#[cfg(target_os = "windows")]
use winsafe::{self as w, co, prelude::*}; use winsafe::{self as w, co, prelude::*};
const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe"; const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe";
const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe"; const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
const REDIST_2015_2022_ARM64: &str = "https://aka.ms/vs/17/release/vc_redist.arm64.exe"; const REDIST_2015_2022_ARM64: &str = "https://aka.ms/vs/17/release/vc_redist.arm64.exe";
@@ -307,7 +307,7 @@ fn get_dotnet_base_path(runtime_arch: RuntimeArch, runtime_type: DotnetRuntimeTy
// it's easy to check if we're looking for x86 dotnet because it's always in the same place. // it's easy to check if we're looking for x86 dotnet because it's always in the same place.
if runtime_arch == RuntimeArch::X86 { if runtime_arch == RuntimeArch::X86 {
let pf32 = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::ProgramFilesX86, co::KF::DONT_UNEXPAND, None)?; let pf32 = super::known_path::get_program_files_x86()?;
let join = Path::new(&pf32).join("dotnet").join(dotnet_path); let join = Path::new(&pf32).join("dotnet").join(dotnet_path);
let result = join.to_str().ok_or_else(|| anyhow!("Unable to convert path to string."))?; let result = join.to_str().ok_or_else(|| anyhow!("Unable to convert path to string."))?;
return Ok(result.to_string()); return Ok(result.to_string());
@@ -315,7 +315,7 @@ fn get_dotnet_base_path(runtime_arch: RuntimeArch, runtime_type: DotnetRuntimeTy
// this only works in a 64 bit process, otherwise it throws // this only works in a 64 bit process, otherwise it throws
#[cfg(not(target_arch = "x86"))] #[cfg(not(target_arch = "x86"))]
let pf64 = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::ProgramFilesX64, co::KF::DONT_UNEXPAND, None)?; let pf64 = super::known_path::get_program_files_x64()?;
// set by WOW64 for x86 processes. https://learn.microsoft.com/windows/win32/winprog64/wow64-implementation-details // set by WOW64 for x86 processes. https://learn.microsoft.com/windows/win32/winprog64/wow64-implementation-details
#[cfg(target_arch = "x86")] #[cfg(target_arch = "x86")]

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@ use std::{
sync::mpsc::{self, Receiver, Sender}, sync::mpsc::{self, Receiver, Sender},
thread, thread,
}; };
use w::WString; use winsafe::guard::DeleteObjectGuard;
use winsafe::{self as w, co, guard::DeleteObjectGuard, gui, prelude::*}; use winsafe::{self as w, co, gui, prelude::*, WString};
const TMR_GIF: usize = 1; const TMR_GIF: usize = 1;
const MSG_NOMESSAGE: i16 = -99; const MSG_NOMESSAGE: i16 = -99;

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
use windows::core::{HSTRING, PCWSTR, PWSTR};
pub fn string_to_u16<P: AsRef<str>>(input: P) -> Vec<u16> {
let input = input.as_ref();
input.encode_utf16().chain(Some(0)).collect::<Vec<u16>>()
}
pub fn pwstr_to_string(input: PWSTR) -> Result<String> {
unsafe {
let hstring = input.to_hstring()?;
let string = hstring.to_string_lossy();
Ok(string.trim_end_matches('\0').to_string())
}
}
pub fn pcwstr_to_string(input: PCWSTR) -> Result<String> {
unsafe {
let hstring = input.to_hstring()?;
let string = hstring.to_string_lossy();
Ok(string.trim_end_matches('\0').to_string())
}
}
pub fn u16_to_string<T: AsRef<[u16]>>(input: T) -> Result<String> {
let input = input.as_ref();
let hstring = HSTRING::from_wide(input)?;
let string = hstring.to_string_lossy();
Ok(string.trim_end_matches('\0').to_string())
}

View File

@@ -1,22 +1,24 @@
use crate::shared::{self, runtime_arch::RuntimeArch};
use anyhow::{anyhow, Result};
use normpath::PathExt;
use std::{ use std::{
os::windows::process::CommandExt, os::windows::process::CommandExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command as Process, process::Command as Process,
time::Duration, time::Duration,
}; };
use anyhow::{anyhow, Result};
use normpath::PathExt;
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
use windows::core::PCWSTR;
use windows::Win32::Storage::FileSystem::GetLongPathNameW;
use windows::Win32::System::SystemInformation::{VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_FLAGS};
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use windows::{ use windows::Win32::{
core::PCWSTR, Foundation::{self, GetLastError},
Win32::{ System::Threading::CreateMutexW,
Foundation::{self, GetLastError},
System::Threading::CreateMutexW,
},
}; };
use winsafe::{self as w, co};
use crate::shared::{self, runtime_arch::RuntimeArch};
use crate::windows::strings::{string_to_u16, u16_to_string};
pub fn run_hook(app: &shared::bundle::Manifest, root_path: &PathBuf, hook_name: &str, timeout_secs: u64) -> bool { pub fn run_hook(app: &shared::bundle::Manifest, root_path: &PathBuf, hook_name: &str, timeout_secs: u64) -> bool {
let sw = simple_stopwatch::Stopwatch::start_new(); let sw = simple_stopwatch::Stopwatch::start_new();
@@ -76,16 +78,63 @@ impl Drop for MutexDropGuard {
pub fn create_global_mutex(app: &shared::bundle::Manifest) -> Result<MutexDropGuard> { pub fn create_global_mutex(app: &shared::bundle::Manifest) -> Result<MutexDropGuard> {
let mutex_name = format!("velopack-{}", &app.id); let mutex_name = format!("velopack-{}", &app.id);
info!("Attempting to open global system mutex: '{}'", &mutex_name); info!("Attempting to open global system mutex: '{}'", &mutex_name);
let encoded = mutex_name.encode_utf16().chain([0u16]).collect::<Vec<u16>>(); let encodedu16 = super::strings::string_to_u16(mutex_name);
let pw = PCWSTR(encoded.as_ptr()); let encoded = PCWSTR(encodedu16.as_ptr());
let mutex = unsafe { CreateMutexW(None, true, pw) }?; let mutex = unsafe { CreateMutexW(None, true, encoded) }?;
match unsafe { GetLastError() } { match unsafe { GetLastError() } {
Foundation::ERROR_SUCCESS => Ok(MutexDropGuard { mutex }), Foundation::ERROR_SUCCESS => Ok(MutexDropGuard { mutex }),
Foundation::ERROR_ALREADY_EXISTS => Err(anyhow!("Another installer or updater for this application is running, quit that process and try again.")), Foundation::ERROR_ALREADY_EXISTS => {
Err(anyhow!("Another installer or updater for this application is running, quit that process and try again."))
}
err => Err(anyhow!("Unable to create global mutex. Error code {:?}", err)), err => Err(anyhow!("Unable to create global mutex. Error code {:?}", err)),
} }
} }
pub fn expand_environment_strings<P: AsRef<str>>(input: P) -> Result<String> {
use windows::Win32::System::Environment::ExpandEnvironmentStringsW;
let encoded_u16 = super::strings::string_to_u16(input);
let encoded = PCWSTR(encoded_u16.as_ptr());
let mut buffer_size = unsafe { ExpandEnvironmentStringsW(encoded, None) };
if buffer_size == 0 {
return Err(anyhow!(windows::core::Error::from_win32()));
}
let mut buffer: Vec<u16> = vec![0; buffer_size as usize];
buffer_size = unsafe { ExpandEnvironmentStringsW(encoded, Some(&mut buffer)) };
if buffer_size == 0 {
return Err(anyhow!(windows::core::Error::from_win32()));
}
super::strings::u16_to_string(buffer)
}
#[test]
fn test_expand_environment_strings() {
assert_eq!(expand_environment_strings("%windir%").unwrap(), "C:\\Windows");
assert_eq!(expand_environment_strings("%windir%\\system32").unwrap(), "C:\\Windows\\system32");
assert_eq!(expand_environment_strings("%windir%\\system32\\").unwrap(), "C:\\Windows\\system32\\");
}
pub fn get_long_path<P: AsRef<str>>(str: P) -> Result<String> {
let str = str.as_ref().to_string();
let str = string_to_u16(str);
let str = PCWSTR(str.as_ptr());
// SAFETY: str is a valid wide string, this call will return required size of buffer
let len = unsafe { GetLongPathNameW(str, None) };
if len == 0 {
return Err(anyhow!(windows::core::Error::from_win32()));
}
let mut vec = vec![0u16; len as usize];
let len = unsafe { GetLongPathNameW(str, Some(vec.as_mut_slice())) };
if len == 0 {
return Err(anyhow!(windows::core::Error::from_win32()));
}
let result = u16_to_string(vec)?;
Ok(result)
}
pub fn is_sub_path<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, parent: P2) -> Result<bool> { pub fn is_sub_path<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, parent: P2) -> Result<bool> {
let path = path.as_ref().to_string_lossy().to_lowercase(); let path = path.as_ref().to_string_lossy().to_lowercase();
let parent = parent.as_ref().to_string_lossy().to_lowercase(); let parent = parent.as_ref().to_string_lossy().to_lowercase();
@@ -104,8 +153,8 @@ pub fn is_sub_path<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, parent: P2) -> Re
return Ok(true); return Ok(true);
} }
let path = w::ExpandEnvironmentStrings(&path)?; let path = expand_environment_strings(&path)?;
let parent = w::ExpandEnvironmentStrings(&parent)?; let parent = expand_environment_strings(&parent)?;
let path = Path::new(&path); let path = Path::new(&path);
let parent = Path::new(&parent); let parent = Path::new(&parent);
@@ -119,8 +168,24 @@ pub fn is_sub_path<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, parent: P2) -> Re
} }
// calls GetFullPathNameW // calls GetFullPathNameW
let path = path.normalize_virtually()?.as_path().to_string_lossy().to_lowercase(); let path = path.normalize().or_else(|_| path.normalize_virtually())?;
let parent = parent.normalize_virtually()?.as_path().to_string_lossy().to_lowercase(); let parent = parent.normalize().or_else(|_| parent.normalize_virtually())?;
let mut path = path.as_path().to_string_lossy().to_string();
let mut parent = parent.as_path().to_string_lossy().to_string();
// calls GetLongPathNameW
match get_long_path(&path) {
Ok(p) => path = p,
Err(e) => warn!("Failed to get long path for '{}': {}", path, e),
}
match get_long_path(&parent) {
Ok(p) => parent = p,
Err(e) => warn!("Failed to get long path for '{}': {}", parent, e),
}
path = path.to_lowercase();
parent = parent.to_lowercase();
let path = PathBuf::from(path); let path = PathBuf::from(path);
let parent = PathBuf::from(parent); let parent = PathBuf::from(parent);
@@ -169,10 +234,7 @@ fn test_is_sub_path_works_with_non_existing_paths() {
let path = PathBuf::from(r"C:\AppData\JamLogic"); let path = PathBuf::from(r"C:\AppData\JamLogic");
let parent = PathBuf::from(r"C:\AppData\JamLogicDev"); let parent = PathBuf::from(r"C:\AppData\JamLogicDev");
assert!(!is_sub_path(&path, &parent).unwrap()); assert!(!is_sub_path(&path, &parent).unwrap());
assert!(!is_sub_path(&parent, &path).unwrap());
let path = PathBuf::from(r"C:\AppData\JamLogicDev");
let parent = PathBuf::from(r"C:\AppData\JamLogic");
assert!(!is_sub_path(&path, &parent).unwrap());
} }
#[test] #[test]
@@ -202,15 +264,58 @@ fn test_is_sub_path_works_with_empty_paths() {
assert!(!is_sub_path(&path, &parent).unwrap()); assert!(!is_sub_path(&path, &parent).unwrap());
} }
// Version condition mask constants defined as per Windows SDK
const VER_GREATER_EQUAL: u8 = 3;
const VER_MINORVERSION: VER_FLAGS = VER_FLAGS(0x0000001);
const VER_MAJORVERSION: VER_FLAGS = VER_FLAGS(0x0000002);
const VER_BUILDNUMBER: VER_FLAGS = VER_FLAGS(0x0000004);
const VER_SERVICEPACKMAJOR: VER_FLAGS = VER_FLAGS(0x0000020);
fn is_os_version_or_greater_internal(major: u16, minor: u16, build: u16, service_pack: u16) -> bool {
let flags = VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR;
unsafe {
let mut mask: u64 = 0;
mask = VerSetConditionMask(mask, VER_MAJORVERSION, VER_GREATER_EQUAL);
mask = VerSetConditionMask(mask, VER_MINORVERSION, VER_GREATER_EQUAL);
mask = VerSetConditionMask(mask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
mask = VerSetConditionMask(mask, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
let mut osvi: OSVERSIONINFOEXW = Default::default();
osvi.dwMajorVersion = major.into();
osvi.dwMinorVersion = minor.into();
osvi.dwBuildNumber = build.into();
osvi.wServicePackMajor = service_pack.into();
VerifyVersionInfoW(&mut osvi, flags, mask).is_ok()
}
}
pub fn is_windows_10_or_greater() -> bool {
is_os_version_or_greater_internal(10, 0, 0, 0)
}
pub fn is_windows_7_sp1_or_greater() -> bool {
is_os_version_or_greater_internal(6, 1, 0, 1)
}
pub fn is_windows_8_or_greater() -> bool {
is_os_version_or_greater_internal(6, 2, 0, 0)
}
pub fn is_windows_8_1_or_greater() -> bool {
is_os_version_or_greater_internal(6, 3, 0, 0)
}
pub fn is_os_version_or_greater(version: &str) -> Result<bool> { pub fn is_os_version_or_greater(version: &str) -> Result<bool> {
let (mut major, mut minor, mut build, _) = shared::parse_version(version)?; let (mut major, mut minor, mut build, _) = shared::parse_version(version)?;
if major < 8 { if major < 8 {
return Ok(w::IsWindows7OrGreater()?); return Ok(is_windows_7_sp1_or_greater());
} }
if major == 8 { if major == 8 {
return Ok(if minor >= 1 { w::IsWindows8Point1OrGreater()? } else { w::IsWindows8OrGreater()? }); return Ok(if minor >= 1 { is_windows_8_or_greater() } else { is_windows_8_1_or_greater() });
} }
// https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions // https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions
@@ -222,20 +327,7 @@ pub fn is_os_version_or_greater(version: &str) -> Result<bool> {
minor = 0; minor = 0;
} }
if major == 10 && build <= 0 { Ok(is_os_version_or_greater_internal(major.try_into()?, minor.try_into()?, build.try_into()?, 0))
return Ok(w::IsWindows10OrGreater()?);
}
let mut mask: u64 = 0;
mask = w::VerSetConditionMask(mask, co::VER_MASK::MAJORVERSION, co::VER_COND::GREATER_EQUAL);
mask = w::VerSetConditionMask(mask, co::VER_MASK::MINORVERSION, co::VER_COND::GREATER_EQUAL);
mask = w::VerSetConditionMask(mask, co::VER_MASK::BUILDNUMBER, co::VER_COND::GREATER_EQUAL);
let mut osvi: w::OSVERSIONINFOEX = Default::default();
osvi.dwMajorVersion = major;
osvi.dwMinorVersion = minor;
osvi.dwBuildNumber = build;
return Ok(w::VerifyVersionInfo(&mut osvi, co::VER_MASK::MAJORVERSION | co::VER_MASK::MINORVERSION | co::VER_MASK::BUILDNUMBER, mask)?);
} }
#[test] #[test]

View File

@@ -6,6 +6,7 @@ use std::{fs, path::Path, path::PathBuf};
use tempfile::tempdir; use tempfile::tempdir;
use velopack::*; use velopack::*;
use velopack::logging::trace_logger;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winsafe::{self as w, co}; use winsafe::{self as w, co};
@@ -13,16 +14,26 @@ use winsafe::{self as w, co};
#[test] #[test]
pub fn test_install_apply_uninstall() { pub fn test_install_apply_uninstall() {
dialogs::set_silent(true); dialogs::set_silent(true);
trace_logger();
let fixtures = find_fixtures(); let fixtures = find_fixtures();
let app_id = "AvaloniaCrossPlat"; let app_id = "AvaloniaCrossPlat";
let pkg_name = "AvaloniaCrossPlat-1.0.11-win-full.nupkg"; let pkg_name = "AvaloniaCrossPlat-1.0.11-win-full.nupkg";
let startmenu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None).unwrap(); let start_menu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None).unwrap();
let lnk_path = Path::new(&startmenu).join("Programs").join(format!("{}.lnk", app_id)); let start_menu = Path::new(&start_menu).join("Programs");
if lnk_path.exists() { let desktop = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Desktop, co::KF::DONT_UNEXPAND, None).unwrap();
fs::remove_file(&lnk_path).unwrap(); let desktop = Path::new(&desktop);
}
let lnk_start_1 = start_menu.join(format!("{}.lnk", app_id));
let lnk_desktop_1 = desktop.join(format!("{}.lnk", app_id));
let lnk_start_2 = start_menu.join(format!("{}.lnk", "AvaloniaCross Updated"));
let lnk_desktop_2 = desktop.join(format!("{}.lnk", "AvaloniaCross Updated"));
let _ = fs::remove_file(&lnk_start_1);
let _ = fs::remove_file(&lnk_desktop_1);
let _ = fs::remove_file(&lnk_start_2);
let _ = fs::remove_file(&lnk_desktop_2);
let nupkg = fixtures.join(pkg_name); let nupkg = fixtures.join(pkg_name);
@@ -30,26 +41,38 @@ pub fn test_install_apply_uninstall() {
let tmp_buf = tmp_dir.path().to_path_buf(); let tmp_buf = tmp_dir.path().to_path_buf();
commands::install(Some(&nupkg), Some(&tmp_buf)).unwrap(); commands::install(Some(&nupkg), Some(&tmp_buf)).unwrap();
assert!(lnk_path.exists()); assert!(!lnk_desktop_1.exists()); // desktop is created during update
assert!(lnk_start_1.exists());
assert!(tmp_buf.join("Update.exe").exists()); assert!(tmp_buf.join("Update.exe").exists());
assert!(tmp_buf.join("current").join("AvaloniaCrossPlat.exe").exists()); assert!(tmp_buf.join("current").join("AvaloniaCrossPlat.exe").exists());
assert!(tmp_buf.join("current").join("sq.version").exists()); assert!(tmp_buf.join("current").join("sq.version").exists());
let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap(); let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap();
assert_eq!(app_id, app.id); assert_eq!(app_id, app.id);
assert!(semver::Version::parse("1.0.11").unwrap() == app.version); assert_eq!(semver::Version::parse("1.0.11").unwrap(), app.version);
let pkg_name_apply = "AvaloniaCrossPlat-1.0.15-win-full.nupkg"; let pkg_name_apply = "AvaloniaCrossPlat-1.0.15-win-full.nupkg";
let nupkg_apply = fixtures.join(pkg_name_apply); let nupkg_apply = fixtures.join(pkg_name_apply);
commands::apply(&root_dir, &app, false, shared::OperationWait::NoWait, Some(&nupkg_apply), None, false).unwrap(); commands::apply(&root_dir, &app, false, shared::OperationWait::NoWait, Some(&nupkg_apply), None, false).unwrap();
// shortcuts are renamed, and desktop is created
assert!(!lnk_desktop_1.exists());
assert!(!lnk_start_1.exists());
assert!(lnk_desktop_2.exists());
assert!(lnk_start_2.exists());
let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap(); let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap();
assert!(semver::Version::parse("1.0.15").unwrap() == app.version); assert_eq!(semver::Version::parse("1.0.15").unwrap(), app.version);
commands::uninstall(&root_dir, &app, false).unwrap(); commands::uninstall(&root_dir, &app, false).unwrap();
assert!(!tmp_buf.join("current").exists()); assert!(!tmp_buf.join("current").exists());
assert!(tmp_buf.join(".dead").exists()); assert!(tmp_buf.join(".dead").exists());
assert!(!lnk_path.exists());
assert!(!lnk_desktop_1.exists());
assert!(!lnk_start_1.exists());
assert!(!lnk_desktop_2.exists());
assert!(!lnk_start_2.exists());
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]

View File

@@ -37,7 +37,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
// add nuspec metadata // add nuspec metadata
ExtraNuspecMetadata["runtimeDependencies"] = GetRuntimeDependencies(); ExtraNuspecMetadata["runtimeDependencies"] = GetRuntimeDependencies();
ExtraNuspecMetadata["shortcutLocations"] = GetShortcutLocations(); ExtraNuspecMetadata["shortcutLocations"] = GetShortcutLocations();
ExtraNuspecMetadata["shortcutAmuid"] = Utility.CreateGuidFromHash(Options.PackId).ToString(); ExtraNuspecMetadata["shortcutAmuid"] = Utility.GetAppUserModelId(Options.PackId);
// copy files to temp dir, so we can modify them // copy files to temp dir, so we can modify them
var dir = TempDir.CreateSubdirectory("PreprocessPackDirWin"); var dir = TempDir.CreateSubdirectory("PreprocessPackDirWin");

View File

@@ -52,12 +52,10 @@ public class WindowsPackCommand : PackCommand
.SetHidden() .SetHidden()
.SetDefault(10); .SetDefault(10);
//AddOption<string>((v) => Shortcuts = v, "--shortcuts") AddOption<string>((v) => Shortcuts = v, "--shortcuts")
// .SetDescription("List of locations to install shortcuts to during setup.") .SetDescription("List of locations to install shortcuts to during setup.")
// .SetArgumentHelpName("LOC") .SetArgumentHelpName("LOC")
// .SetDefault("Desktop,StartMenuRoot") .SetDefault("Desktop,StartMenuRoot");
// .SetHidden(true); // this argument currently has no effect
Shortcuts = "Desktop,StartMenuRoot";
if (VelopackRuntimeInfo.IsWindows) { if (VelopackRuntimeInfo.IsWindows) {
var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n") var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n")

View File

@@ -205,6 +205,7 @@ namespace Velopack
if (String.IsNullOrWhiteSpace(channel) || channel.ToLower() == "win") { if (String.IsNullOrWhiteSpace(channel) || channel.ToLower() == "win") {
return "RELEASES"; return "RELEASES";
} }
// all other cases the RELEASES file includes the channel name. // all other cases the RELEASES file includes the channel name.
return $"RELEASES-{channel.ToLower()}"; return $"RELEASES-{channel.ToLower()}";
} }
@@ -212,10 +213,14 @@ namespace Velopack
public static void Retry(this Action block, int retries = 4, int retryDelay = 250, ILogger? logger = null) public static void Retry(this Action block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
{ {
Retry(() => { Retry(
block(); () => {
return true; block();
}, retries, retryDelay, logger); return true;
},
retries,
retryDelay,
logger);
} }
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger? logger = null) public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
@@ -237,10 +242,14 @@ namespace Velopack
public static Task RetryAsync(this Func<Task> block, int retries = 4, int retryDelay = 250, ILogger? logger = null) public static Task RetryAsync(this Func<Task> block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
{ {
return RetryAsync(async () => { return RetryAsync(
await block().ConfigureAwait(false); async () => {
return true; await block().ConfigureAwait(false);
}, retries, retryDelay, logger); return true;
},
retries,
retryDelay,
logger);
} }
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger? logger = null) public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger? logger = null)
@@ -276,11 +285,12 @@ namespace Velopack
{ {
return Task.WhenAll( return Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(degreeOfParallelism) from partition in Partitioner.Create(source).GetPartitions(degreeOfParallelism)
select Task.Run(async () => { select Task.Run(
using (partition) async () => {
while (partition.MoveNext()) using (partition)
await body(partition.Current).ConfigureAwait(false); while (partition.MoveNext())
})); await body(partition.Current).ConfigureAwait(false);
}));
} }
/// <summary> /// <summary>
@@ -299,6 +309,7 @@ namespace Velopack
else else
safeName.Append('_'); safeName.Append('_');
} }
safeFileName = safeName.ToString(); safeFileName = safeName.ToString();
} }
@@ -465,13 +476,16 @@ namespace Velopack
// retry a few times. if a directory in this tree is open in Windows Explorer, // retry a few times. if a directory in this tree is open in Windows Explorer,
// it might be locked for a little while WE cleans up handles // it might be locked for a little while WE cleans up handles
try { try {
Retry(() => { Retry(
try { () => {
deleteMe(); try {
} catch (DirectoryNotFoundException) { deleteMe();
return; // good! } catch (DirectoryNotFoundException) {
} return; // good!
}, retries: 4, retryDelay: 50); }
},
retries: 4,
retryDelay: 50);
} catch (Exception ex) { } catch (Exception ex) {
logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'"); logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'");
throw; throw;
@@ -519,10 +533,9 @@ namespace Velopack
// .OrderByDescending(x => x.Version); // .OrderByDescending(x => x.Version);
//} //}
public static string GetAppUserModelId(string packageId, string exeName) public static string GetAppUserModelId(string packageId)
{ {
return String.Format("com.velopack.{0}.{1}", packageId.Replace(" ", ""), return $"velopack.{packageId}";
exeName.Replace(".exe", "").Replace(" ", ""));
} }
public static bool IsHttpUrl(string urlOrPath) public static bool IsHttpUrl(string urlOrPath)

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@@ -22,6 +23,9 @@ namespace Velopack
/// </summary> /// </summary>
public sealed class VelopackApp public sealed class VelopackApp
{ {
[DllImport("shell32.dll", SetLastError = true)]
private static extern void SetCurrentProcessExplicitAppUserModelID([MarshalAs(UnmanagedType.LPWStr)] string AppID);
internal static ILogger DefaultLogger { get; private set; } = NullLogger.Instance; internal static ILogger DefaultLogger { get; private set; } = NullLogger.Instance;
internal static IVelopackLocator? DefaultLocator { get; private set; } internal static IVelopackLocator? DefaultLocator { get; private set; }
@@ -165,6 +169,12 @@ namespace Velopack
log.Info("Starting Velopack App (Run)."); log.Info("Starting Velopack App (Run).");
if (VelopackRuntimeInfo.IsWindows && locator.AppId != null) {
var appUserModelId = Utility.GetAppUserModelId(locator.AppId);
log.Info($"Setting current process explicit AppUserModelID to '{appUserModelId}'");
SetCurrentProcessExplicitAppUserModelID(appUserModelId);
}
// first, we run any fast exit hooks // first, we run any fast exit hooks
VelopackHook defaultBlock = ((v) => { }); VelopackHook defaultBlock = ((v) => { });
var fastExitlookup = new[] { var fastExitlookup = new[] {
@@ -227,6 +237,7 @@ namespace Velopack
if (package.Type == VelopackAssetType.Full && (package.Version == latestLocal?.Version || package.Version == myVersion)) { if (package.Type == VelopackAssetType.Full && (package.Version == latestLocal?.Version || package.Version == myVersion)) {
continue; continue;
} }
try { try {
log.Info("Removing old package: " + package.FileName); log.Info("Removing old package: " + package.FileName);
var p = Path.Combine(pkgPath, package.FileName); var p = Path.Combine(pkgPath, package.FileName);
@@ -245,6 +256,7 @@ namespace Velopack
log.Error(ex, $"Error occurred executing user defined Velopack hook. (firstrun)"); log.Error(ex, $"Error occurred executing user defined Velopack hook. (firstrun)");
} }
} }
if (restarted) { if (restarted) {
try { try {
_restarted?.Invoke(myVersion); _restarted?.Invoke(myVersion);
@@ -261,4 +273,4 @@ namespace Velopack
} }
} }
} }
} }

View File

@@ -196,6 +196,7 @@ public class WindowsPackTests
PackVersion = version, PackVersion = version,
TargetRuntime = RID.Parse("win-x64"), TargetRuntime = RID.Parse("win-x64"),
PackDirectory = tmpOutput, PackDirectory = tmpOutput,
Shortcuts = "Desktop,StartMenuRoot",
}; };
var runner = GetPackRunner(logger); var runner = GetPackRunner(logger);