mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Shortcut rework / Configure shortcuts on the vpk command line (#165)
This commit is contained in:
550
src/Rust/Cargo.lock
generated
550
src/Rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -52,14 +52,14 @@ regex = "1.10"
|
||||
rand = "0.8"
|
||||
log = "0.4"
|
||||
simplelog = "0.12"
|
||||
clap = "4.4"
|
||||
clap = "4.5"
|
||||
xml = "0.8"
|
||||
semver = "1.0"
|
||||
chrono = "0.4"
|
||||
wait-timeout = "0.2"
|
||||
lazy_static = "1.4"
|
||||
lazy_static = "1.5"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
ureq = { version = "2.9", default-features = false, features = [
|
||||
ureq = { version = "2.10", default-features = false, features = [
|
||||
"native-tls",
|
||||
"gzip",
|
||||
] }
|
||||
@@ -79,6 +79,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
time = "0.3"
|
||||
os_info = "3.8"
|
||||
bitflags = "2.6"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
native-dialog = "0.7"
|
||||
@@ -87,15 +88,15 @@ dialog = "0.3"
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
fs_extra = "1.2"
|
||||
fs_extra = "1.3"
|
||||
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 = [
|
||||
"gif",
|
||||
"jpeg",
|
||||
"png",
|
||||
] }
|
||||
windows = { version = "0.56", default-features = false, features = [
|
||||
windows = { version = "0.57", default-features = false, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_System_Com",
|
||||
@@ -104,6 +105,7 @@ windows = { version = "0.56", default-features = false, features = [
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_Variant",
|
||||
"Win32_System_Environment",
|
||||
"Win32_Storage_EnhancedStorage",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
@@ -111,24 +113,20 @@ windows = { version = "0.56", default-features = false, features = [
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
] }
|
||||
windows-sys = { version = "0.52", default-features = false, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Storage",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Kernel",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Wdk",
|
||||
"Wdk_System",
|
||||
"Wdk_System_Threading",
|
||||
] }
|
||||
normpath = "1.0.1"
|
||||
webview2-com = "0.30"
|
||||
normpath = "1.2"
|
||||
webview2-com = "0.31"
|
||||
libloading = "0.8"
|
||||
strsim = "0.11"
|
||||
same-file = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.9"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
max_width = 160
|
||||
max_width = 140
|
||||
use_small_heuristics = "Max"
|
||||
indent_style = "Visual"
|
||||
unstable_features = true
|
||||
|
||||
@@ -16,7 +16,15 @@ fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<
|
||||
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
|
||||
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 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(())
|
||||
}
|
||||
|
||||
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 manifest = bundle.read_manifest()?;
|
||||
let new_app = bundle.read_manifest()?;
|
||||
|
||||
let found_version = (manifest.version).to_owned();
|
||||
info!("Applying package to current: {}", found_version);
|
||||
let found_version = (new_app.version).to_owned();
|
||||
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.");
|
||||
}
|
||||
|
||||
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 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_old = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
|
||||
|
||||
// open a dialog showing progress...
|
||||
let (mut tx, _) = mpsc::channel::<i16>();
|
||||
if !dialogs::get_silent() {
|
||||
let title = format!("{} Update", &manifest.title);
|
||||
let message = format!("Installing update {}...", &manifest.version);
|
||||
let title = format!("{} Update", &new_app.title);
|
||||
let message = format!("Installing update {}...", &new_app.version);
|
||||
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);
|
||||
|
||||
// second, run application hooks (but don't care if it fails)
|
||||
if runhooks {
|
||||
crate::windows::run_hook(app, root_path, "--veloapp-obsolete", 15);
|
||||
if run_hooks {
|
||||
crate::windows::run_hook(old_app, root_path, "--veloapp-obsolete", 15);
|
||||
} else {
|
||||
info!("Skipping --veloapp-obsolete hook.");
|
||||
}
|
||||
|
||||
// third, we try _REALLY HARD_ to stop the package
|
||||
let _ = shared::force_stop_package(root_path);
|
||||
if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&app.title, ¤t_dir) {
|
||||
if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&old_app.title, ¤t_dir) {
|
||||
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);
|
||||
|
||||
info!("Showing error dialog...");
|
||||
let title = format!("{} Update", &manifest.title);
|
||||
let title = format!("{} Update", &new_app.title);
|
||||
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);
|
||||
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
|
||||
// seventh, we run the post-install hooks
|
||||
if runhooks {
|
||||
crate::windows::run_hook(&manifest, &root_path, "--veloapp-updated", 15);
|
||||
if run_hooks {
|
||||
crate::windows::run_hook(&new_app, &root_path, "--veloapp-updated", 15);
|
||||
} else {
|
||||
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!
|
||||
info!("Package applied successfully.");
|
||||
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_old);
|
||||
action?;
|
||||
Ok(manifest)
|
||||
Ok(new_app)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ use crate::{
|
||||
shared::{self, bundle, runtime_arch::RuntimeArch},
|
||||
windows,
|
||||
};
|
||||
use ::windows::core::PCWSTR;
|
||||
use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use memmap2::Mmap;
|
||||
use pretty_bytes_rust::pretty_bytes;
|
||||
@@ -11,15 +13,14 @@ use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use winsafe::{self as w, co};
|
||||
|
||||
pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Result<()> {
|
||||
let osinfo = os_info::get();
|
||||
let osarch = RuntimeArch::from_current_system();
|
||||
info!("OS: {osinfo}, Arch={osarch:#?}");
|
||||
|
||||
if !w::IsWindows7OrGreater()? {
|
||||
bail!("This installer requires Windows 7 or later and cannot run.");
|
||||
if !windows::is_windows_7_sp1_or_greater() {
|
||||
bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
|
||||
}
|
||||
|
||||
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() {
|
||||
(install_to.unwrap().clone(), false)
|
||||
} 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)
|
||||
};
|
||||
|
||||
@@ -66,18 +67,26 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
|
||||
// do we have enough disk space?
|
||||
let (compressed_size, extracted_size) = pkg.calculate_size();
|
||||
let required_space = compressed_size + extracted_size + (50 * 1000 * 1000); // archive + velopack overhead
|
||||
|
||||
let mut free_space: u64 = 0;
|
||||
w::GetDiskFreeSpaceEx(Some(&root_path_str), None, None, Some(&mut free_space))?;
|
||||
if free_space < required_space {
|
||||
bail!(
|
||||
"{} requires at least {} disk space to be installed. There is only {} available.",
|
||||
&app.title,
|
||||
pretty_bytes(required_space, None),
|
||||
pretty_bytes(free_space, None)
|
||||
);
|
||||
let root_pcwstr = windows::strings::string_to_u16(root_path_str);
|
||||
let root_pcwstr: PCWSTR = PCWSTR(root_pcwstr.as_ptr());
|
||||
if let Ok(()) = unsafe { GetDiskFreeSpaceExW(root_pcwstr, None, None, Some(&mut free_space)) } {
|
||||
if free_space < required_space {
|
||||
bail!(
|
||||
"{} requires at least {} disk space to be installed. There is only {} available.",
|
||||
&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?
|
||||
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.");
|
||||
|
||||
shared::force_stop_package(&root_path)
|
||||
.map_err(|z| anyhow!("Failed to stop application ({}), please close the application and try running the installer again.", z))?;
|
||||
shared::force_stop_package(&root_path).map_err(|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));
|
||||
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.");
|
||||
}
|
||||
|
||||
info!("Creating new default shortcuts...");
|
||||
let _ = windows::create_default_lnks(&root_path, &app);
|
||||
info!("Creating shortcuts...");
|
||||
windows::create_or_update_manifest_lnks(&root_path, &app, None);
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -13,7 +13,13 @@ use std::{
|
||||
};
|
||||
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) {
|
||||
Ok(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...");
|
||||
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...");
|
||||
shared::delete_app_prefixed_folders(root_dir)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@ pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Resu
|
||||
// run uninstall hook
|
||||
windows::run_hook(&app, root_path, "--veloapp-uninstall", 60);
|
||||
|
||||
if let Err(e) = windows::remove_all_shortcuts_for_root_dir(&root_path) {
|
||||
error!("Unable to remove shortcuts ({}).", e);
|
||||
// finished_with_errors = true;
|
||||
}
|
||||
// remove all shortcuts pointing to the app
|
||||
windows::remove_all_shortcuts_for_root_dir(&root_path);
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -373,7 +373,10 @@ impl BundleInfo<'_> {
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i)?;
|
||||
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());
|
||||
}
|
||||
@@ -396,6 +399,8 @@ pub struct Manifest {
|
||||
pub os: String,
|
||||
pub os_min_version: String,
|
||||
pub channel: String,
|
||||
pub shortcut_locations: String,
|
||||
pub shortcut_amuid: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -436,7 +441,8 @@ impl Manifest {
|
||||
let uninstall_cmd = format!("\"{}\" --uninstall", 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;
|
||||
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()))?;
|
||||
@@ -454,7 +460,8 @@ impl Manifest {
|
||||
}
|
||||
pub fn remove_uninstall_entry(&self) -> Result<()> {
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -497,6 +504,10 @@ pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
|
||||
obj.os_min_version = text;
|
||||
} else if el_name == "channel" {
|
||||
obj.channel = text;
|
||||
} else if el_name == "shortcutLocations" {
|
||||
obj.shortcut_locations = text;
|
||||
} else if el_name == "shortcutAmuid" {
|
||||
obj.shortcut_amuid = text;
|
||||
}
|
||||
}
|
||||
Ok(XmlEvent::EndElement { .. }) => {
|
||||
|
||||
@@ -51,7 +51,7 @@ fn get_download_agent() -> Result<ureq::Agent> {
|
||||
let mut tls_builder = native_tls::TlsConnector::builder();
|
||||
|
||||
#[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.");
|
||||
@@ -65,7 +65,10 @@ fn get_download_agent() -> Result<ureq::Agent> {
|
||||
|
||||
#[test]
|
||||
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]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use ::windows::Win32::System::ProcessStatus::EnumProcesses;
|
||||
use ::windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
@@ -7,10 +9,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command as Process,
|
||||
};
|
||||
use windows::Win32::System::ProcessStatus::EnumProcesses;
|
||||
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
use windows_sys::Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation};
|
||||
use windows_sys::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
|
||||
use windows::Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation};
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
|
||||
use winsafe::{self as w, co, prelude::*};
|
||||
|
||||
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 {
|
||||
AffinityMask: 0,
|
||||
BasePriority: 0,
|
||||
ExitStatus: 0,
|
||||
ExitStatus: Default::default(),
|
||||
InheritedFromUniqueProcessId: 0,
|
||||
PebBaseAddress: std::ptr::null_mut(),
|
||||
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_size = std::mem::size_of::<PROCESS_BASIC_INFORMATION>() as u32;
|
||||
let hr = unsafe { NtQueryInformationProcess(handle, basic_info, info_ptr, info_size, return_length_ptr) };
|
||||
|
||||
if hr != 0 {
|
||||
return Err(anyhow!("Failed to query process information: {}", hr));
|
||||
let hres = unsafe { NtQueryInformationProcess(handle, basic_info, info_ptr, info_size, return_length_ptr) };
|
||||
if hres.is_err() {
|
||||
return Err(anyhow!("Failed to query process information: {:?}", hres));
|
||||
}
|
||||
|
||||
if info.InheritedFromUniqueProcessId <= 1 {
|
||||
@@ -346,7 +345,7 @@ fn get_all_packages(root_path: &PathBuf) -> Vec<EntryNameInfo> {
|
||||
|
||||
#[test]
|
||||
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 rustup = path.join(".rustup");
|
||||
|
||||
|
||||
61
src/Rust/src/windows/known_path.rs
Normal file
61
src/Rust/src/windows/known_path.rs
Normal 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())
|
||||
}
|
||||
@@ -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
|
||||
/// our binary in the user's download folder.
|
||||
#[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.
|
||||
// This is only necessary prior to Windows 10 RS1. See build.rs for details.
|
||||
unsafe {
|
||||
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||
let _ = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ pub mod mitigate;
|
||||
pub mod prerequisite;
|
||||
pub mod runtimes;
|
||||
pub mod splash;
|
||||
pub mod known_path;
|
||||
pub mod strings;
|
||||
|
||||
mod self_delete;
|
||||
mod shortcuts;
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::shared::{bundle, dialogs, download};
|
||||
|
||||
use anyhow::Result;
|
||||
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> {
|
||||
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());
|
||||
|
||||
info!("Downloading {} missing pre-requisites...", missing.len());
|
||||
|
||||
@@ -5,8 +5,8 @@ use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
use std::process::Command as Process;
|
||||
use std::{collections::HashMap, fs, path::Path};
|
||||
#[cfg(target_os = "windows")]
|
||||
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_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";
|
||||
@@ -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.
|
||||
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 result = join.to_str().ok_or_else(|| anyhow!("Unable to convert path 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
|
||||
#[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
|
||||
#[cfg(target_arch = "x86")]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ use std::{
|
||||
sync::mpsc::{self, Receiver, Sender},
|
||||
thread,
|
||||
};
|
||||
use w::WString;
|
||||
use winsafe::{self as w, co, guard::DeleteObjectGuard, gui, prelude::*};
|
||||
use winsafe::guard::DeleteObjectGuard;
|
||||
use winsafe::{self as w, co, gui, prelude::*, WString};
|
||||
|
||||
const TMR_GIF: usize = 1;
|
||||
const MSG_NOMESSAGE: i16 = -99;
|
||||
|
||||
30
src/Rust/src/windows/strings.rs
Normal file
30
src/Rust/src/windows/strings.rs
Normal 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())
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
use crate::shared::{self, runtime_arch::RuntimeArch};
|
||||
use anyhow::{anyhow, Result};
|
||||
use normpath::PathExt;
|
||||
use std::{
|
||||
os::windows::process::CommandExt,
|
||||
path::{Path, PathBuf},
|
||||
process::Command as Process,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use normpath::PathExt;
|
||||
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::{
|
||||
core::PCWSTR,
|
||||
Win32::{
|
||||
Foundation::{self, GetLastError},
|
||||
System::Threading::CreateMutexW,
|
||||
},
|
||||
use windows::Win32::{
|
||||
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 {
|
||||
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> {
|
||||
let mutex_name = format!("velopack-{}", &app.id);
|
||||
info!("Attempting to open global system mutex: '{}'", &mutex_name);
|
||||
let encoded = mutex_name.encode_utf16().chain([0u16]).collect::<Vec<u16>>();
|
||||
let pw = PCWSTR(encoded.as_ptr());
|
||||
let mutex = unsafe { CreateMutexW(None, true, pw) }?;
|
||||
let encodedu16 = super::strings::string_to_u16(mutex_name);
|
||||
let encoded = PCWSTR(encodedu16.as_ptr());
|
||||
let mutex = unsafe { CreateMutexW(None, true, encoded) }?;
|
||||
match unsafe { GetLastError() } {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let path = path.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);
|
||||
}
|
||||
|
||||
let path = w::ExpandEnvironmentStrings(&path)?;
|
||||
let parent = w::ExpandEnvironmentStrings(&parent)?;
|
||||
let path = expand_environment_strings(&path)?;
|
||||
let parent = expand_environment_strings(&parent)?;
|
||||
|
||||
let path = Path::new(&path);
|
||||
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
|
||||
let path = path.normalize_virtually()?.as_path().to_string_lossy().to_lowercase();
|
||||
let parent = parent.normalize_virtually()?.as_path().to_string_lossy().to_lowercase();
|
||||
let path = path.normalize().or_else(|_| path.normalize_virtually())?;
|
||||
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 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 parent = PathBuf::from(r"C:\AppData\JamLogicDev");
|
||||
assert!(!is_sub_path(&path, &parent).unwrap());
|
||||
|
||||
let path = PathBuf::from(r"C:\AppData\JamLogicDev");
|
||||
let parent = PathBuf::from(r"C:\AppData\JamLogic");
|
||||
assert!(!is_sub_path(&path, &parent).unwrap());
|
||||
assert!(!is_sub_path(&parent, &path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -202,15 +264,58 @@ fn test_is_sub_path_works_with_empty_paths() {
|
||||
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> {
|
||||
let (mut major, mut minor, mut build, _) = shared::parse_version(version)?;
|
||||
|
||||
if major < 8 {
|
||||
return Ok(w::IsWindows7OrGreater()?);
|
||||
return Ok(is_windows_7_sp1_or_greater());
|
||||
}
|
||||
|
||||
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
|
||||
@@ -222,20 +327,7 @@ pub fn is_os_version_or_greater(version: &str) -> Result<bool> {
|
||||
minor = 0;
|
||||
}
|
||||
|
||||
if major == 10 && build <= 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)?);
|
||||
Ok(is_os_version_or_greater_internal(major.try_into()?, minor.try_into()?, build.try_into()?, 0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{fs, path::Path, path::PathBuf};
|
||||
use tempfile::tempdir;
|
||||
use velopack::*;
|
||||
|
||||
use velopack::logging::trace_logger;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winsafe::{self as w, co};
|
||||
|
||||
@@ -13,16 +14,26 @@ use winsafe::{self as w, co};
|
||||
#[test]
|
||||
pub fn test_install_apply_uninstall() {
|
||||
dialogs::set_silent(true);
|
||||
trace_logger();
|
||||
|
||||
let fixtures = find_fixtures();
|
||||
|
||||
let app_id = "AvaloniaCrossPlat";
|
||||
let pkg_name = "AvaloniaCrossPlat-1.0.11-win-full.nupkg";
|
||||
|
||||
let startmenu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None).unwrap();
|
||||
let lnk_path = Path::new(&startmenu).join("Programs").join(format!("{}.lnk", app_id));
|
||||
if lnk_path.exists() {
|
||||
fs::remove_file(&lnk_path).unwrap();
|
||||
}
|
||||
let start_menu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None).unwrap();
|
||||
let start_menu = Path::new(&start_menu).join("Programs");
|
||||
let desktop = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Desktop, co::KF::DONT_UNEXPAND, None).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);
|
||||
|
||||
@@ -30,26 +41,38 @@ pub fn test_install_apply_uninstall() {
|
||||
let tmp_buf = tmp_dir.path().to_path_buf();
|
||||
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("current").join("AvaloniaCrossPlat.exe").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();
|
||||
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 nupkg_apply = fixtures.join(pkg_name_apply);
|
||||
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();
|
||||
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();
|
||||
assert!(!tmp_buf.join("current").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")]
|
||||
|
||||
@@ -37,7 +37,7 @@ public class WindowsPackCommandRunner : PackageBuilder<WindowsPackOptions>
|
||||
// add nuspec metadata
|
||||
ExtraNuspecMetadata["runtimeDependencies"] = GetRuntimeDependencies();
|
||||
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
|
||||
var dir = TempDir.CreateSubdirectory("PreprocessPackDirWin");
|
||||
|
||||
@@ -52,12 +52,10 @@ public class WindowsPackCommand : PackCommand
|
||||
.SetHidden()
|
||||
.SetDefault(10);
|
||||
|
||||
//AddOption<string>((v) => Shortcuts = v, "--shortcuts")
|
||||
// .SetDescription("List of locations to install shortcuts to during setup.")
|
||||
// .SetArgumentHelpName("LOC")
|
||||
// .SetDefault("Desktop,StartMenuRoot")
|
||||
// .SetHidden(true); // this argument currently has no effect
|
||||
Shortcuts = "Desktop,StartMenuRoot";
|
||||
AddOption<string>((v) => Shortcuts = v, "--shortcuts")
|
||||
.SetDescription("List of locations to install shortcuts to during setup.")
|
||||
.SetArgumentHelpName("LOC")
|
||||
.SetDefault("Desktop,StartMenuRoot");
|
||||
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n")
|
||||
|
||||
@@ -205,6 +205,7 @@ namespace Velopack
|
||||
if (String.IsNullOrWhiteSpace(channel) || channel.ToLower() == "win") {
|
||||
return "RELEASES";
|
||||
}
|
||||
|
||||
// all other cases the RELEASES file includes the channel name.
|
||||
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)
|
||||
{
|
||||
Retry(() => {
|
||||
block();
|
||||
return true;
|
||||
}, retries, retryDelay, logger);
|
||||
Retry(
|
||||
() => {
|
||||
block();
|
||||
return true;
|
||||
},
|
||||
retries,
|
||||
retryDelay,
|
||||
logger);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return RetryAsync(async () => {
|
||||
await block().ConfigureAwait(false);
|
||||
return true;
|
||||
}, retries, retryDelay, logger);
|
||||
return RetryAsync(
|
||||
async () => {
|
||||
await block().ConfigureAwait(false);
|
||||
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)
|
||||
@@ -276,11 +285,12 @@ namespace Velopack
|
||||
{
|
||||
return Task.WhenAll(
|
||||
from partition in Partitioner.Create(source).GetPartitions(degreeOfParallelism)
|
||||
select Task.Run(async () => {
|
||||
using (partition)
|
||||
while (partition.MoveNext())
|
||||
await body(partition.Current).ConfigureAwait(false);
|
||||
}));
|
||||
select Task.Run(
|
||||
async () => {
|
||||
using (partition)
|
||||
while (partition.MoveNext())
|
||||
await body(partition.Current).ConfigureAwait(false);
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -299,6 +309,7 @@ namespace Velopack
|
||||
else
|
||||
safeName.Append('_');
|
||||
}
|
||||
|
||||
safeFileName = safeName.ToString();
|
||||
}
|
||||
|
||||
@@ -465,13 +476,16 @@ namespace Velopack
|
||||
// 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
|
||||
try {
|
||||
Retry(() => {
|
||||
try {
|
||||
deleteMe();
|
||||
} catch (DirectoryNotFoundException) {
|
||||
return; // good!
|
||||
}
|
||||
}, retries: 4, retryDelay: 50);
|
||||
Retry(
|
||||
() => {
|
||||
try {
|
||||
deleteMe();
|
||||
} catch (DirectoryNotFoundException) {
|
||||
return; // good!
|
||||
}
|
||||
},
|
||||
retries: 4,
|
||||
retryDelay: 50);
|
||||
} catch (Exception ex) {
|
||||
logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'");
|
||||
throw;
|
||||
@@ -519,10 +533,9 @@ namespace Velopack
|
||||
// .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(" ", ""),
|
||||
exeName.Replace(".exe", "").Replace(" ", ""));
|
||||
return $"velopack.{packageId}";
|
||||
}
|
||||
|
||||
public static bool IsHttpUrl(string urlOrPath)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -22,6 +23,9 @@ namespace Velopack
|
||||
/// </summary>
|
||||
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 IVelopackLocator? DefaultLocator { get; private set; }
|
||||
@@ -165,6 +169,12 @@ namespace Velopack
|
||||
|
||||
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
|
||||
VelopackHook defaultBlock = ((v) => { });
|
||||
var fastExitlookup = new[] {
|
||||
@@ -227,6 +237,7 @@ namespace Velopack
|
||||
if (package.Type == VelopackAssetType.Full && (package.Version == latestLocal?.Version || package.Version == myVersion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
log.Info("Removing old package: " + 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)");
|
||||
}
|
||||
}
|
||||
|
||||
if (restarted) {
|
||||
try {
|
||||
_restarted?.Invoke(myVersion);
|
||||
@@ -261,4 +273,4 @@ namespace Velopack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,7 @@ public class WindowsPackTests
|
||||
PackVersion = version,
|
||||
TargetRuntime = RID.Parse("win-x64"),
|
||||
PackDirectory = tmpOutput,
|
||||
Shortcuts = "Desktop,StartMenuRoot",
|
||||
};
|
||||
|
||||
var runner = GetPackRunner(logger);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user