Refactor string handling to use OsString where possible

This commit is contained in:
Caelan Sayler
2025-05-25 01:06:25 +01:00
parent 3c7d82aa22
commit 661cc022f8
33 changed files with 642 additions and 650 deletions

View File

@@ -1,7 +1,7 @@
use crate::shared::{self, OperationWait};
use velopack::{locator, locator::VelopackLocator, constants};
use anyhow::{bail, Result};
use std::path::PathBuf;
use std::{ffi::OsString, path::PathBuf};
use velopack::{constants, locator, locator::VelopackLocator};
#[cfg(target_os = "linux")]
use super::apply_linux_impl::apply_package_impl;
@@ -15,7 +15,7 @@ pub fn apply<'a>(
restart: bool,
wait: OperationWait,
package: Option<&PathBuf>,
exe_args: Option<Vec<&str>>,
exe_args: Option<Vec<OsString>>,
run_hooks: bool,
) -> Result<VelopackLocator> {
shared::operation_wait(wait);
@@ -25,10 +25,12 @@ pub fn apply<'a>(
match package {
Some(package) => {
info!("Getting ready to apply package to {} ver {}: {}",
locator.get_manifest_id(),
locator.get_manifest_version_full_string(),
package.to_string_lossy());
info!(
"Getting ready to apply package to {} ver {}: {:?}",
locator.get_manifest_id(),
locator.get_manifest_version_full_string(),
package
);
match apply_package_impl(&locator, &package, run_hooks) {
Ok(applied_locator) => {
info!("Package version {} applied successfully.", applied_locator.get_manifest_version_full_string());

View File

@@ -6,7 +6,7 @@ use velopack::{bundle, locator::VelopackLocator};
pub fn apply_package_impl<'a>(locator: &VelopackLocator, pkg: &PathBuf, _runhooks: bool) -> Result<VelopackLocator> {
// on linux, the current "dir" is actually an AppImage file which we need to replace.
info!("Loading bundle from {}", pkg.to_string_lossy());
info!("Loading bundle from {:?}", pkg);
let mut bundle = bundle::load_bundle_from_file(pkg)?;
let manifest = bundle.read_manifest()?;
let temp_path = locator.get_temp_dir_rand16().to_string_lossy().to_string();

View File

@@ -4,7 +4,7 @@ use crate::{
windows::{self, splash},
};
use anyhow::{bail, Context, Result};
use std::{fs, path::PathBuf};
use std::{ffi::OsString, fs, path::PathBuf};
use std::{sync::mpsc, time::Duration};
use velopack::{bundle::load_bundle_from_file, constants, locator::VelopackLocator, process};
@@ -51,8 +51,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_
} else {
info!("Re-launching as administrator to update in {:?}", root_path);
let package_string = package.to_string_lossy().to_string();
let args = vec!["--norestart".to_string(), "--package".to_string(), package_string];
let args: Vec<OsString> = vec!["--norestart".into(), "--package".into(), package.into()];
let exe_path = std::env::current_exe()?;
let work_dir: Option<String> = None; // same as this process
let process_handle = process::run_process_as_admin(&exe_path, args, work_dir, false)?;
@@ -108,7 +107,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_
// }
// fourth, we make as backup of the current dir to temp_path_old
info!("Backing up current dir to {}", &temp_path_old.to_string_lossy());
info!("Backing up current dir to {:?}", &temp_path_old);
shared::retry_io_ex(|| fs::rename(&current_dir, &temp_path_old), 1000, 10)
.context("Unable to start the update, because one or more running processes prevented it. Try again later, or if the issue persists, restart your computer.")?;
@@ -121,7 +120,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_
// fifth, we try to replace the current dir with temp_path_new
// if this fails we will yolo a rollback...
info!("Replacing current dir with {}", &temp_path_new.to_string_lossy());
info!("Replacing current dir with {:?}", &temp_path_new);
shared::retry_io_ex(|| fs::rename(&temp_path_new, &current_dir), 1000, 30)
.context("Unable to complete the update, and the app was left in a broken state. You may need to re-install or repair this application manually.")?;

View File

@@ -3,25 +3,24 @@ use crate::{
shared::{self},
windows,
};
use velopack::bundle::BundleZip;
use velopack::locator::*;
use velopack::constants;
use velopack::locator::*;
use velopack::{bundle::BundleZip, wide_strings::string_to_wide};
use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
use anyhow::{anyhow, bail, Result};
use pretty_bytes_rust::pretty_bytes;
use std::{
ffi::OsString,
fs::{self},
path::{Path, PathBuf},
};
use ::windows::core::PCWSTR;
use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Option<Vec<&str>>) -> Result<()> {
pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Option<Vec<OsString>>) -> Result<()> {
// find and parse nuspec
info!("Reading package manifest...");
let app = pkg.read_manifest()?;
info!("Package manifest loaded successfully.");
info!(" Package ID: {}", &app.id);
info!(" Package Version: {}", &app.version);
@@ -49,17 +48,15 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
shared::retry_io(|| fs::create_dir_all(&root_path))?;
}
let root_path_str = root_path.to_str().unwrap();
info!("Installation Directory: {:?}", root_path_str);
info!("Installation Directory: {:?}", root_path);
// 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;
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)) } {
let root_pcwstr = string_to_wide(&root_path);
if let Ok(()) = unsafe { GetDiskFreeSpaceExW(root_pcwstr.as_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.",
@@ -85,7 +82,7 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
bail!("This application ({}) does not support your CPU architecture.", &app.machine_architecture);
}
let mut root_path_renamed = String::new();
let mut root_path_renamed: Option<PathBuf> = None;
// does the target directory exist and have files? (eg. already installed)
if !shared::is_dir_empty(&root_path) {
// the target directory is not empty, and not dead
@@ -100,20 +97,22 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
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(16));
info!("Renaming existing directory to '{}' to allow rollback...", root_path_renamed);
let renamed = root_path.with_extension(shared::random_string(16));
info!("Renaming existing directory to '{:?}' to allow rollback...", renamed);
shared::retry_io(|| fs::rename(&root_path, &root_path_renamed)).map_err(|_| {
shared::retry_io(|| fs::rename(&root_path, &renamed)).map_err(|_| {
anyhow!(
"Failed to remove existing application directory, please close the application and try running the installer again. \
If the issue persists, try uninstalling first via Programs & Features, or restarting your computer."
)
})?;
root_path_renamed = Some(renamed);
}
info!("Preparing and cleaning installation directory...");
remove_dir_all::ensure_empty_dir(&root_path)?;
info!("Acquiring lock...");
let paths = create_config_from_root_dir(&root_path);
let locator = VelopackLocator::new_with_manifest(paths, app);
@@ -134,17 +133,17 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
if install_result.is_ok() {
info!("Installation completed successfully!");
if !root_path_renamed.is_empty() {
if let Some(renamed) = root_path_renamed {
info!("Removing rollback directory...");
let _ = shared::retry_io(|| fs::remove_dir_all(&root_path_renamed));
let _ = shared::retry_io(|| remove_dir_all::remove_dir_all(&renamed));
}
} else {
error!("Installation failed!");
if !root_path_renamed.is_empty() {
if let Some(renamed) = root_path_renamed {
info!("Rolling back installation...");
let _ = shared::force_stop_package(&root_path);
let _ = shared::retry_io(|| fs::remove_dir_all(&root_path));
let _ = shared::retry_io(|| fs::rename(&root_path_renamed, &root_path));
let _ = shared::retry_io(|| remove_dir_all::remove_dir_all(&root_path));
let _ = shared::retry_io(|| fs::rename(&renamed, &root_path));
}
install_result?;
}
@@ -152,7 +151,12 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
Ok(())
}
fn install_impl(pkg: &mut BundleZip, locator: &VelopackLocator, tx: &std::sync::mpsc::Sender<i16>, start_args: Option<Vec<&str>>) -> Result<()> {
fn install_impl(
pkg: &mut BundleZip,
locator: &VelopackLocator,
tx: &std::sync::mpsc::Sender<i16>,
start_args: Option<Vec<OsString>>,
) -> Result<()> {
info!("Starting installation!");
// all application paths

View File

@@ -12,11 +12,11 @@ pub fn zstd_patch_single<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>(old_
let output_file = output_file.as_ref();
if !old_file.exists() {
bail!("Old file does not exist: {}", old_file.to_string_lossy());
bail!("Old file does not exist: {:?}", old_file);
}
if !patch_file.exists() {
bail!("Patch file does not exist: {}", patch_file.to_string_lossy());
bail!("Patch file does not exist: {:?}", patch_file);
}
let dict = fs::read(old_file)?;
@@ -62,7 +62,7 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>(
let output_file = output_file.as_ref().to_path_buf();
if !old_file.exists() {
bail!("Old file does not exist: {}", old_file.to_string_lossy());
bail!("Old file does not exist: {:?}", old_file);
}
if delta_files.is_empty() {
@@ -71,13 +71,13 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>(
for delta_file in &delta_files {
if !delta_file.exists() {
bail!("Delta file does not exist: {}", delta_file.to_string_lossy());
bail!("Delta file does not exist: {:?}", delta_file);
}
}
let time = simple_stopwatch::Stopwatch::start_new();
info!("Extracting base package for delta patching: {}", temp_dir.to_string_lossy());
info!("Extracting base package for delta patching: {:?}", temp_dir);
let work_dir = temp_dir.join("_work");
fs::create_dir_all(&work_dir)?;
fastzip::extract_to_directory(&old_file, &work_dir, None)?;
@@ -85,7 +85,7 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>(
info!("Base package extracted. {} delta packages to apply.", delta_files.len());
for (i, delta_file) in delta_files.iter().enumerate() {
info!("{}: extracting apply delta patch: {}", i, delta_file.to_string_lossy());
info!("{}: extracting apply delta patch: {:?}", i, delta_file);
let delta_dir = temp_dir.join(format!("delta_{}", i));
fs::create_dir_all(&delta_dir)?;
fastzip::extract_to_directory(&delta_file, &delta_dir, None)?;
@@ -152,7 +152,7 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>(
}
}
info!("All delta patches applied. Asembling output package at: {}", output_file.to_string_lossy());
info!("All delta patches applied. Asembling output package at: {:?}", output_file);
fastzip::compress_directory(&work_dir, &output_file, fastzip::CompressionLevel::fast())?;

View File

@@ -1,22 +1,16 @@
use crate::shared::{self, OperationWait};
use anyhow::Result;
use std::ffi::OsString;
use velopack::locator::LocationContext;
#[allow(unused_variables, unused_imports)]
#[allow(unused_variables)]
pub fn start(
wait: OperationWait,
context: LocationContext,
exe_name: Option<&String>,
exe_args: Option<Vec<&str>>,
legacy_args: Option<&String>,
exe_name: Option<&OsString>,
exe_args: Option<Vec<OsString>>,
legacy_args: Option<&OsString>,
) -> Result<()> {
use anyhow::bail;
#[cfg(target_os = "windows")]
if legacy_args.is_some() && exe_args.is_some() {
bail!("Cannot use both legacy args and new args format.");
}
shared::operation_wait(wait);
#[cfg(target_os = "windows")]

View File

@@ -4,19 +4,16 @@ use crate::{
windows as win,
};
use anyhow::{anyhow, bail, Result};
use std::os::windows::process::CommandExt;
use std::{
fs,
path::Path,
path::PathBuf,
process::Command as Process,
ffi::{OsStr, OsString},
os::windows::process::CommandExt,
};
use velopack::{bundle::Manifest, constants};
use std::{fs, path::Path, path::PathBuf, process::Command as Process};
use velopack::locator::{self, LocationContext, VelopackLocator};
use velopack::{bundle::Manifest, constants};
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
enum LocatorResult
{
enum LocatorResult {
Normal(VelopackLocator),
Legacy(PathBuf, Manifest),
}
@@ -34,18 +31,18 @@ impl LocatorResult {
LocatorResult::Legacy(_, manifest) => manifest.clone(),
}
}
pub fn get_current_dir(&self) -> PathBuf {
match self {
LocatorResult::Normal(locator) => locator.get_current_bin_dir(),
LocatorResult::Legacy(path, _) => path.join("current"),
}
}
pub fn get_exe_to_start(&self, name: Option<&String>) -> Result<PathBuf> {
pub fn get_exe_to_start<P: AsRef<OsStr>>(&self, name: Option<P>) -> Result<PathBuf> {
let current_dir = self.get_current_dir();
if let Some(name) = name {
Ok(Path::new(&current_dir).join(name))
Ok(Path::new(&current_dir).join(name.as_ref()))
} else {
match self {
LocatorResult::Normal(locator) => Ok(locator.get_main_exe_path()),
@@ -71,10 +68,10 @@ fn legacy_locator(context: LocationContext) -> Result<LocatorResult> {
let parent_dir = my_exe.parent().expect("Unable to determine parent directory");
let packages_dir = parent_dir.join("packages");
if let Some((path, manifest)) = locator::find_latest_full_package(&packages_dir) {
info!("Found full package to read: {}", path.to_string_lossy());
info!("Found full package to read: {:?}", path);
Ok(LocatorResult::Legacy(parent_dir.to_path_buf(), manifest))
} else {
bail!("Unable to locate app manifest or full package in {}.", packages_dir.to_string_lossy());
bail!("Unable to locate app manifest or full package in {:?}.", packages_dir);
}
}
}
@@ -82,10 +79,14 @@ fn legacy_locator(context: LocationContext) -> Result<LocatorResult> {
pub fn start_impl(
context: LocationContext,
exe_name: Option<&String>,
exe_args: Option<Vec<&str>>,
legacy_args: Option<&String>,
exe_name: Option<&OsString>,
exe_args: Option<Vec<OsString>>,
legacy_args: Option<&OsString>,
) -> Result<()> {
if legacy_args.is_some() && exe_args.is_some() {
anyhow::bail!("Cannot use both legacy args and new args format at the same time.");
}
let locator = legacy_locator(context)?;
let root_dir = locator.get_root_dir();
let manifest = locator.get_manifest();
@@ -113,17 +114,17 @@ pub fn start_impl(
fn start_regular(
locator: LocatorResult,
exe_name: Option<&String>,
exe_args: Option<Vec<&str>>,
legacy_args: Option<&String>,
exe_name: Option<&OsString>,
exe_args: Option<Vec<OsString>>,
legacy_args: Option<&OsString>,
) -> Result<()> {
// we can't just run the normal start_package command, because legacy squirrel might provide
// we can't just run the normal start_package command, because legacy squirrel might provide
// an "exe name" to restart which no longer exists in the package
let exe_to_execute = locator.get_exe_to_start(exe_name)?;
if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{:?}'", exe_to_execute);
}
let current = locator.get_current_dir();
info!("About to launch: '{:?}' in dir '{:?}'", exe_to_execute, current);
@@ -143,13 +144,14 @@ fn start_regular(
fn try_legacy_migration(root_dir: &PathBuf, manifest: &Manifest) -> Result<VelopackLocator> {
info!("This is a legacy app. Will try and upgrade it now.");
// if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder,
// meaning we can not clean up properly.
std::env::set_current_dir(&root_dir)?;
let path_config = locator::create_config_from_root_dir(root_dir);
let package = locator::find_latest_full_package(&path_config.PackagesDir).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
let package =
locator::find_latest_full_package(&path_config.PackagesDir).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate...");
let _ = shared::force_stop_package(&root_dir);

View File

@@ -19,7 +19,7 @@ pub fn uninstall(locator: &VelopackLocator, delete_self: bool) -> Result<()> {
// remove all shortcuts pointing to the app
windows::remove_all_shortcuts_for_root_dir(&root_path);
info!("Removing directory '{}'", root_path.to_string_lossy());
info!("Removing directory '{:?}'", root_path);
let _ = remove_dir_all::remove_dir_contents(&root_path);
if let Err(e) = windows::registry::remove_uninstall_entry(&locator) {

View File

@@ -7,6 +7,7 @@ extern crate log;
use anyhow::{bail, Result};
use clap::{arg, value_parser, Command};
use memmap2::Mmap;
use std::ffi::OsString;
use std::fs::File;
use std::{env, path::PathBuf};
use velopack_bins::*;
@@ -83,7 +84,7 @@ fn main_inner() -> Result<()> {
let debug = matches.get_one::<PathBuf>("debug");
let install_to = matches.get_one::<PathBuf>("installto");
let exe_args: Option<Vec<&str>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect());
let exe_args = matches.get_many::<OsString>("EXE_ARGS").map(|v| v.map(|f| f.to_os_string()).collect());
info!("Starting Velopack Setup ({})", env!("NGBV_VERSION"));
info!(" Location: {:?}", env::current_exe()?);

View File

@@ -1,10 +1,10 @@
use super::{dialogs_common::*, dialogs_const::*};
use crate::windows::strings::string_to_wide;
use anyhow::Result;
use std::path::PathBuf;
use velopack::{
bundle::Manifest,
locator::{auto_locate_app_manifest, LocationContext},
wide_strings::{string_to_wide, string_to_wide_opt},
};
use windows::{
core::HRESULT,
@@ -74,7 +74,7 @@ pub fn show_uninstall_complete_with_errors_dialog(app_title: &str, log_path: Opt
let setup_name = string_to_wide(format!("{} Uninstall", app_title));
let instruction = string_to_wide(format!("{} uninstall has completed with errors.", app_title));
let content = string_to_wide(
"There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again.",
"There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again."
);
let mut config = TASKDIALOGCONFIG::default();
@@ -224,7 +224,7 @@ pub fn generate_confirm(
ico: DialogIcon,
) -> Result<DialogResult> {
let hparent = unsafe { GetDesktopWindow() };
let mut ok_text_buf = ok_text.map(string_to_wide);
let mut ok_text_buf = string_to_wide_opt(ok_text);
let mut custom_btns = if let Some(ok_text_buf) = ok_text_buf.as_mut() {
let td_btn = TASKDIALOG_BUTTON { nButtonID: IDOK.0, pszButtonText: ok_text_buf.as_pcwstr() };
vec![td_btn]
@@ -246,7 +246,7 @@ pub fn generate_confirm(
let title_buf = string_to_wide(title);
tdc.pszWindowTitle = title_buf.as_pcwstr();
let mut header_buf = header.map(string_to_wide);
let mut header_buf = string_to_wide_opt(header);
if let Some(header_buf) = header_buf.as_mut() {
tdc.pszMainInstruction = header_buf.as_pcwstr();
}

View File

@@ -24,7 +24,7 @@ pub fn wait_for_parent_to_exit(ms_to_wait: u32) -> Result<()> {
}
pub fn force_stop_package<P: AsRef<Path>>(root_dir: P) -> Result<()> {
let root_dir = root_dir.as_ref().to_string_lossy().to_string();
let root_dir = root_dir.as_ref();
let command = format!("quit app \"{}\"", root_dir);
Process::new("/usr/bin/osascript").arg("-e").arg(command).spawn().map_err(|z| anyhow!("Failed to stop application ({}).", z))?;
Ok(())

View File

@@ -1,4 +1,3 @@
use crate::windows::strings;
use ::windows::{
core::PWSTR,
Win32::{
@@ -11,10 +10,11 @@ use regex::Regex;
use semver::Version;
use std::{
collections::HashMap,
ffi::OsString,
fs,
path::{Path, PathBuf},
};
use velopack::{locator::VelopackLocator, process};
use velopack::{locator::VelopackLocator, process, wide_strings::wide_to_os_string};
// https://github.com/nushell/nushell/blob/4458aae3d41517d74ce1507ad3e8cd94021feb16/crates/nu-system/src/windows.rs#L593
fn get_pids() -> Result<Vec<u32>> {
@@ -55,12 +55,9 @@ unsafe fn get_processes_running_in_directory<P: AsRef<Path>>(dir: P) -> Result<V
continue;
}
let full_path = strings::u16_to_string(&full_path_vec);
if let Err(_) = full_path {
continue;
}
let full_path = PathBuf::from(full_path.unwrap());
let slice = &full_path_vec[..full_path_len as usize];
let full_path = wide_to_os_string(slice);
let full_path = PathBuf::from(full_path);
if let Ok(is_subpath) = crate::windows::is_sub_path(&full_path, dir) {
if is_subpath {
oup.push((pid, full_path, process));
@@ -93,21 +90,20 @@ fn _force_stop_package<P: AsRef<Path>>(root_dir: P) -> Result<()> {
Ok(())
}
pub fn start_package(locator: &VelopackLocator, exe_args: Option<Vec<&str>>, set_env: Option<&str>) -> Result<()> {
pub fn start_package(locator: &VelopackLocator, exe_args: Option<Vec<OsString>>, set_env: Option<&str>) -> Result<()> {
let current = locator.get_current_bin_dir();
let exe_to_execute = locator.get_main_exe_path();
if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy());
bail!("Unable to find executable to start: '{:?}'", exe_to_execute);
}
let args: Vec<String> = exe_args.unwrap_or_default().iter().map(|s| s.to_string()).collect();
let mut environment = HashMap::new();
if let Some(env_var) = set_env {
debug!("Setting environment variable: {}={}", env_var, "true");
environment.insert(env_var.to_string(), "true".to_string());
}
process::run_process(exe_to_execute, args, Some(current), true, Some(environment))?;
process::run_process(exe_to_execute, exe_args.unwrap_or_default(), Some(current), true, Some(environment))?;
Ok(())
}

View File

@@ -37,7 +37,7 @@ fn main() -> ExitCode {
args.insert(0, "start".to_owned());
args.insert(1, "--".to_owned());
info!("Stub about to start Update.exe ({}) with args: {:?}", update_exe.to_string_lossy(), args);
info!("Stub about to start Update.exe ({:?}) with args: {:?}", update_exe, args);
const CREATE_NO_WINDOW: u32 = 0x08000000;
match Process::new(update_exe).args(args).creation_flags(CREATE_NO_WINDOW).spawn() {

View File

@@ -6,6 +6,7 @@ extern crate log;
use anyhow::{anyhow, bail, Result};
use clap::{arg, value_parser, ArgAction, ArgMatches, Command};
use std::ffi::OsString;
use std::{env, path::PathBuf};
use velopack::locator::{auto_locate_app_manifest, LocationContext};
use velopack::logging::*;
@@ -22,15 +23,15 @@ fn root_command() -> Command {
.arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update").hide(true))
.arg(arg!(--waitPid <PID> "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32)))
.arg(arg!(-p --package <FILE> "Update package to apply").value_parser(value_parser!(PathBuf)))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..).value_parser(value_parser!(OsString)))
)
.subcommand(Command::new("start")
.about("Starts the currently installed version of the application")
.arg(arg!(-a --args <ARGS> "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1))
.arg(arg!(-a --args <ARGS> "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1).value_parser(value_parser!(OsString)))
.arg(arg!(-w --wait "Wait for the parent process to terminate before starting the application").hide(true))
.arg(arg!(--waitPid <PID> "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32)))
.arg(arg!([EXE_NAME] "The optional name of the binary to execute"))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..))
.arg(arg!([EXE_NAME] "The optional name of the binary to execute").value_parser(value_parser!(OsString)))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..).value_parser(value_parser!(OsString)))
.long_flag_aliases(vec!["processStart", "processStartAndWait"])
)
.subcommand(Command::new("patch")
@@ -158,8 +159,8 @@ fn main() -> Result<()> {
info!("--");
info!("Starting Velopack Updater ({})", env!("NGBV_VERSION"));
info!(" Location: {}", env::current_exe()?.to_string_lossy());
info!(" CWD: {}", env::current_dir()?.to_string_lossy());
info!(" Location: {:?}", env::current_exe()?);
info!(" CWD: {:?}", env::current_dir()?);
info!(" Verbose: {}", verbose);
info!(" Silent: {}", silent);
info!(" Log File: {:?}", log_file);
@@ -220,11 +221,11 @@ fn patch(_context: LocationContext, matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn get_exe_args(matches: &ArgMatches) -> Option<Vec<&str>> {
matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect())
fn get_exe_args(matches: &ArgMatches) -> Option<Vec<OsString>> {
matches.get_many::<OsString>("EXE_ARGS").map(|v| v.map(|f| f.to_os_string()).collect())
}
fn get_apply_args(matches: &ArgMatches) -> (OperationWait, bool, Option<&PathBuf>, Option<Vec<&str>>) {
fn get_apply_args(matches: &ArgMatches) -> (OperationWait, bool, Option<&PathBuf>, Option<Vec<OsString>>) {
let restart = !get_flag_or_false(&matches, "norestart");
let package = matches.get_one::<PathBuf>("package");
let exe_args = get_exe_args(matches);
@@ -246,9 +247,9 @@ fn apply(context: LocationContext, matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn get_start_args(matches: &ArgMatches) -> (OperationWait, Option<&String>, Option<&String>, Option<Vec<&str>>) {
let legacy_args = matches.get_one::<String>("args");
let exe_name = matches.get_one::<String>("EXE_NAME");
fn get_start_args(matches: &ArgMatches) -> (OperationWait, Option<&OsString>, Option<&OsString>, Option<Vec<OsString>>) {
let legacy_args = matches.get_one::<OsString>("args");
let exe_name = matches.get_one::<OsString>("EXE_NAME");
let exe_args = get_exe_args(matches);
let wait = get_op_wait(&matches);
(wait, exe_name, legacy_args, exe_args)
@@ -346,7 +347,7 @@ fn test_start_command_supports_legacy_commands() {
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::NoWait);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, None);
@@ -354,7 +355,7 @@ fn test_start_command_supports_legacy_commands() {
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::NoWait);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, None);
@@ -362,7 +363,7 @@ fn test_start_command_supports_legacy_commands() {
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, None);
@@ -370,40 +371,40 @@ fn test_start_command_supports_legacy_commands() {
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, None);
assert_eq!(exe_args, Some(vec!["Foo=Bar"]));
assert_eq!(exe_args, Some(vec!["Foo=Bar".into()]));
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "-a", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, Some(&"myarg".into()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "-a", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, Some(&"myarg".into()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "--processStartArgs", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, Some(&"myarg".into()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "hello.exe", "--process-start-args", "myarg"];
let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap();
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, Some(&"hello.exe".to_string()));
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(exe_name, Some(&"hello.exe".into()));
assert_eq!(legacy_args, Some(&"myarg".into()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "-a", "myarg"];
@@ -411,7 +412,7 @@ fn test_start_command_supports_legacy_commands() {
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, None);
assert_eq!(legacy_args, Some(&"myarg".to_string()));
assert_eq!(legacy_args, Some(&"myarg".into()));
assert_eq!(exe_args, None);
let command = vec!["Update.exe", "--processStartAndWait", "-a", "-- -c \" asda --aasd"];
@@ -419,6 +420,6 @@ fn test_start_command_supports_legacy_commands() {
let (wait_for_parent, exe_name, legacy_args, exe_args) = get_start_args(matches.subcommand_matches("start").unwrap());
assert_eq!(wait_for_parent, OperationWait::WaitParent);
assert_eq!(exe_name, None);
assert_eq!(legacy_args, Some(&"-- -c \" asda --aasd".to_string()));
assert_eq!(legacy_args, Some(&"-- -c \" asda --aasd".into()));
assert_eq!(exe_args, None);
}

View File

@@ -1,5 +1,6 @@
use anyhow::Result;
use std::path::Path;
use anyhow::{bail, Result};
use std::path::PathBuf;
use velopack::wide_strings::wide_to_os_string;
use windows::{
core::GUID,
Win32::UI::Shell::{
@@ -8,54 +9,60 @@ use windows::{
},
};
fn get_known_folder(rfid: *const GUID) -> Result<String> {
fn get_known_folder(rfid: *const GUID) -> Result<PathBuf> {
unsafe {
let flag = windows::Win32::UI::Shell::KNOWN_FOLDER_FLAG(0);
let result = SHGetKnownFolderPath(rfid, flag, None)?;
super::strings::u16_to_string(result)
if result.is_null() {
bail!("Failed to get known folder path (SHGetKnownFolderPath returned null)");
}
let str = wide_to_os_string(result);
let path = PathBuf::from(str);
Ok(path)
}
}
pub fn get_local_app_data() -> Result<String> {
pub fn get_local_app_data() -> Result<PathBuf> {
get_known_folder(&FOLDERID_LocalAppData)
}
pub fn get_roaming_app_data() -> Result<String> {
pub fn get_roaming_app_data() -> Result<PathBuf> {
get_known_folder(&FOLDERID_RoamingAppData)
}
pub fn get_user_desktop() -> Result<String> {
pub fn get_user_desktop() -> Result<PathBuf> {
get_known_folder(&FOLDERID_Desktop)
}
pub fn get_user_profile() -> Result<String> {
pub fn get_user_profile() -> Result<PathBuf> {
get_known_folder(&FOLDERID_Profile)
}
pub fn get_start_menu() -> Result<String> {
pub fn get_start_menu() -> Result<PathBuf> {
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())
let programs_path = start_menu.join("Programs");
Ok(programs_path)
}
pub fn get_startup() -> Result<String> {
pub fn get_startup() -> Result<PathBuf> {
get_known_folder(&FOLDERID_Startup)
}
pub fn get_downloads() -> Result<String> {
pub fn get_downloads() -> Result<PathBuf> {
get_known_folder(&FOLDERID_Downloads)
}
pub fn get_program_files_x64() -> Result<String> {
pub fn get_program_files_x64() -> Result<PathBuf> {
get_known_folder(&FOLDERID_ProgramFilesX64)
}
pub fn get_program_files_x86() -> Result<String> {
pub fn get_program_files_x86() -> Result<PathBuf> {
get_known_folder(&FOLDERID_ProgramFilesX86)
}
pub fn get_user_pinned() -> Result<String> {
pub fn get_user_pinned() -> Result<PathBuf> {
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())
let pinned_path = pinned_str.join("Microsoft").join("Internet Explorer").join("Quick Launch").join("User Pinned");
Ok(pinned_path)
}

View File

@@ -5,7 +5,6 @@ pub mod runtimes;
pub mod splash;
pub mod known_path;
pub mod strings;
pub mod registry;
pub mod webview2;

View File

@@ -1,9 +1,7 @@
use super::{runtimes, splash};
use crate::shared::dialogs;
use velopack::{bundle, download};
use anyhow::Result;
use std::path::Path;
use velopack::{bundle, download};
pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Option<&semver::Version>) -> Result<bool> {
info!("Checking application pre-requisites...");
@@ -39,7 +37,6 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt
}
let downloads = super::known_path::get_downloads()?;
let downloads = Path::new(downloads.as_str());
info!("Downloading {} missing pre-requisites...", missing.len());
let quiet = dialogs::get_silent();
@@ -47,15 +44,19 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt
for i in 0..missing.len() {
let dep = &missing[i];
let url = dep.get_download_url()?;
let exe_name = downloads.join(dep.get_exe_name());
let exe_path = downloads.join(dep.get_exe_name());
if !exe_name.exists() {
let window_title = if updating_from.is_some() { format!("{} Update", dep.display_name()) } else { format!("{} Setup", dep.display_name()) };
if !exe_path.exists() {
let window_title = if updating_from.is_some() {
format!("{} Update", dep.display_name())
} else {
format!("{} Setup", dep.display_name())
};
let content = format!("Downloading {}...", dep.display_name());
info!(" {}", content);
let tx = splash::show_progress_dialog(window_title, content);
let result = download::download_url_to_file(&url, &exe_name.to_str().unwrap(), |p| {
let result = download::download_url_to_file(&url, &exe_path, |p| {
let _ = tx.send(p);
});
@@ -64,7 +65,7 @@ pub fn prompt_and_install_all_missing(app: &bundle::Manifest, updating_from: Opt
}
info!(" Installing {}...", dep.display_name());
let result = dep.install(exe_name.to_str().unwrap(), quiet)?;
let result = dep.install(&exe_path, quiet)?;
if result == runtimes::RuntimeInstallResult::RestartRequired {
warn!("A restart is required to complete the installation of {}.", dep.display_name());
dialogs::show_restart_required(&app);

View File

@@ -1,5 +1,6 @@
use anyhow::Result;
use chrono::{Datelike, Local as DateTime};
use std::ffi::OsString;
use velopack::locator::VelopackLocator;
use winreg::{enums::*, RegKey};
@@ -12,9 +13,9 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
let app_title = locator.get_manifest_title();
let app_authors = locator.get_manifest_authors();
let root_path_str = locator.get_root_dir_as_string();
let main_exe_path = locator.get_main_exe_path_as_string();
let updater_path = locator.get_update_path_as_string();
let root_path = locator.get_root_dir();
let main_exe_path = locator.get_main_exe_path();
let updater_path = locator.get_update_path();
let folder_size = fs_extra::dir::get_size(locator.get_current_bin_dir()).unwrap_or(0);
let folder_size_kb = folder_size / 1024;
@@ -23,8 +24,13 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
let now = DateTime::now();
let formatted_date = format!("{}{:02}{:02}", now.year(), now.month(), now.day());
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet: String = format!("\"{}\" --uninstall --silent", updater_path);
let mut uninstall_cmd = OsString::from("\"");
uninstall_cmd.push(&updater_path);
uninstall_cmd.push("\" --uninstall");
let mut uninstall_quiet = OsString::from("\"");
uninstall_quiet.push(&updater_path);
uninstall_quiet.push("\" --uninstall --silent");
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (reg_uninstall, _reg_uninstall_disp) = hkcu.create_subkey(UNINSTALL_REGISTRY_KEY)?;
@@ -33,11 +39,11 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
let u32true = 1u32;
let language = 0x0409u32;
reg_app.set_value("DisplayIcon", &main_exe_path)?;
reg_app.set_value("DisplayIcon", &main_exe_path.as_os_str())?;
reg_app.set_value("DisplayName", &app_title)?;
reg_app.set_value("DisplayVersion", &short_version)?;
reg_app.set_value("InstallDate", &formatted_date)?;
reg_app.set_value("InstallLocation", &root_path_str)?;
reg_app.set_value("InstallLocation", &root_path.as_os_str())?;
reg_app.set_value("Publisher", &app_authors)?;
reg_app.set_value("QuietUninstallString", &uninstall_quiet)?;
reg_app.set_value("UninstallString", &uninstall_cmd)?;

View File

@@ -67,7 +67,7 @@ pub enum RuntimeInstallResult {
RestartRequired,
}
fn def_installer_routine(installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
fn def_installer_routine(installer_path: &Path, quiet: bool) -> Result<RuntimeInstallResult> {
let mut args = Vec::new();
args.push("/norestart");
if quiet {
@@ -77,7 +77,7 @@ fn def_installer_routine(installer_path: &str, quiet: bool) -> Result<RuntimeIns
args.push("/showrmui");
}
info!("Running installer: '{}', args={:?}", installer_path, args);
info!("Running installer: '{:?}', args={:?}", installer_path, args);
let mut cmd = Process::new(installer_path).args(&args).spawn()?;
let result: i32 = cmd.wait()?.code().ok_or_else(|| anyhow!("Unable to get installer exit code."))?;
@@ -99,7 +99,7 @@ pub trait RuntimeInfo {
fn display_name(&self) -> &str;
fn is_installed(&self) -> bool;
fn get_download_url(&self) -> Result<String>;
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult>;
fn install(&self, installer_path: &Path, quiet: bool) -> Result<RuntimeInstallResult>;
}
#[derive(Clone, Debug)]
@@ -143,7 +143,7 @@ impl RuntimeInfo for FullFrameworkInfo {
}
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
fn install(&self, installer_path: &Path, quiet: bool) -> Result<RuntimeInstallResult> {
def_installer_routine(installer_path, quiet)
}
}
@@ -204,7 +204,7 @@ impl RuntimeInfo for VCRedistInfo {
false
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
fn install(&self, installer_path: &Path, quiet: bool) -> Result<RuntimeInstallResult> {
def_installer_routine(installer_path, quiet)
}
}
@@ -460,7 +460,7 @@ impl RuntimeInfo for DotnetInfo {
false
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
fn install(&self, installer_path: &Path, quiet: bool) -> Result<RuntimeInstallResult> {
def_installer_routine(installer_path, quiet)
}
}
@@ -549,14 +549,14 @@ impl RuntimeInfo for WebView2Info {
}
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
fn install(&self, installer_path: &Path, quiet: bool) -> Result<RuntimeInstallResult> {
let args = if quiet {
vec!["/silent", "/install"]
} else {
vec!["/install"]
};
info!("Running installer: '{}', args={:?}", installer_path, args);
info!("Running installer: '{:?}', args={:?}", installer_path, args);
let mut cmd = Process::new(installer_path).args(&args).spawn()?;
let result: i32 = cmd.wait()?.code().ok_or_else(|| anyhow!("Unable to get installer exit code."))?;

View File

@@ -5,8 +5,8 @@ use std::time::Duration;
use anyhow::{anyhow, bail, Result};
use glob::glob;
use same_file::is_same_file;
use velopack::{locator::ShortcutLocationFlags, locator::VelopackLocator};
use windows::core::{Interface, GUID, PCWSTR};
use velopack::{locator::ShortcutLocationFlags, locator::VelopackLocator, wide_strings::*};
use windows::core::{Interface, GUID};
use windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID;
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, StructuredStorage::InitPropVariantFromStringVector,
@@ -17,7 +17,7 @@ use windows::Win32::UI::Shell::{
};
use crate::shared as util;
use crate::windows::{known_path as known, strings::*};
use crate::windows::known_path as known;
// https://github.com/vaginessa/PWAsForFirefox/blob/fba68dbcc7ca27b970dc5a278ebdad32e0ab3c83/native/src/integrations/implementation/windows.rs#L28
@@ -52,7 +52,11 @@ unsafe fn create_instance<T: Interface>(clsid: &GUID) -> Result<T> {
}
fn get_shortcut_filename(app_id: &str, app_title: &str) -> String {
let name = if app_title.is_empty() { app_id.to_owned() } else { app_title.to_owned() };
let name = if app_title.is_empty() {
app_id.to_owned()
} else {
app_title.to_owned()
};
let shortcut_file_name = name + ".lnk";
shortcut_file_name
}
@@ -96,8 +100,8 @@ unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_a
let app_title = next_app.get_manifest_title();
let app_authors = next_app.get_manifest_authors();
let app_model_id: Option<String> = next_app.get_manifest_shortcut_amuid();
let app_main_exe = next_app.get_main_exe_path_as_string();
let app_work_dir = next_app.get_current_bin_dir_as_string();
let app_main_exe = next_app.get_main_exe_path();
let app_work_dir = next_app.get_current_bin_dir();
info!("App Model ID: {:?}", app_model_id);
let mut current_shortcuts = unsafe_get_shortcuts_for_root_dir(root_path);
@@ -113,7 +117,7 @@ unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_a
// set the target path to the main exe if it is missing or incorrect
if target_option.is_none() || !PathBuf::from(target_option.unwrap()).exists() {
warn!("Shortcut {} target does not exist, updating to mainExe and setting workdir to current.", lnk.get_link_path());
warn!("Shortcut {:?} target does not exist, updating to mainExe and setting workdir to current.", lnk.get_link_path());
if let Err(e) = lnk.set_target_path(&app_main_exe) {
warn!("Failed to update shortcut target: {}", e);
}
@@ -213,7 +217,7 @@ unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_a
break;
}
if let Err(e) = lnk.save_as(&path.to_string_lossy()) {
if let Err(e) = lnk.save_as(&path) {
warn!("Failed to save shortcut: {}", e);
break;
}
@@ -280,33 +284,34 @@ unsafe fn unsafe_find_best_rename_candidates<P: AsRef<Path>>(
unsafe fn unsafe_get_shortcuts_for_root_dir<P: AsRef<Path>>(root_dir: P) -> Vec<(ShortcutLocationFlags, Lnk)> {
let root_dir = root_dir.as_ref();
info!("Searching for shortcuts containing root: '{}'", root_dir.to_string_lossy());
info!("Searching for shortcuts containing root: '{:?}'", root_dir);
let mut search_paths = Vec::new();
match known::get_user_desktop() {
Ok(user_desktop) => search_paths.push((ShortcutLocationFlags::DESKTOP, format!("{}/*.lnk", user_desktop))),
Ok(user_desktop) => search_paths.push((ShortcutLocationFlags::DESKTOP, user_desktop, "/*.lnk")),
Err(e) => error!("Failed to get user desktop directory, it will not be searched: {}", e),
}
match known::get_startup() {
Ok(startup) => search_paths.push((ShortcutLocationFlags::STARTUP, format!("{}/*.lnk", startup))),
Ok(startup) => search_paths.push((ShortcutLocationFlags::STARTUP, startup, "/*.lnk")),
Err(e) => error!("Failed to get startup directory, it will not be searched: {}", e),
}
match known::get_start_menu() {
// this handles START_MENU and START_MENU_ROOT
Ok(start_menu) => search_paths.push((ShortcutLocationFlags::START_MENU, format!("{}/**/*.lnk", start_menu))),
Ok(start_menu) => search_paths.push((ShortcutLocationFlags::START_MENU, start_menu, "/**/*.lnk")),
Err(e) => error!("Failed to get start menu directory, it will not be searched: {}", e),
}
match known::get_user_pinned() {
Ok(user_pinned) => search_paths.push((ShortcutLocationFlags::USER_PINNED, format!("{}/**/*.lnk", user_pinned))),
Ok(user_pinned) => search_paths.push((ShortcutLocationFlags::USER_PINNED, user_pinned, "/**/*.lnk")),
Err(e) => error!("Failed to get user pinned directory, it will not be searched: {}", e),
}
let mut paths: Vec<(ShortcutLocationFlags, Lnk)> = Vec::new();
for (flag, search_glob) in search_paths {
for (flag, folder_root, search_pattern) in search_paths {
let search_glob = folder_root.to_string_lossy().to_string() + search_pattern;
info!("Searching for shortcuts in: {:?} ({})", flag, search_glob);
if let Ok(glob_paths) = glob(&search_glob) {
for path in glob_paths.filter_map(Result::ok) {
@@ -315,16 +320,16 @@ unsafe fn unsafe_get_shortcuts_for_root_dir<P: AsRef<Path>>(root_dir: P) -> Vec<
Ok(properties) => {
if let Ok(target) = properties.get_target_path() {
if super::is_sub_path(&target, root_dir).unwrap_or(false) {
info!("Selected shortcut for update '{}' because target '{}' matched.", path.to_string_lossy(), target);
info!("Selected shortcut for update '{:?}' because target '{:?}' matched.", path, target);
paths.push((flag, properties));
}
} else if let Ok(work_dir) = properties.get_working_directory() {
if super::is_sub_path(&work_dir, root_dir).unwrap_or(false) {
info!("Selected shortcut for update '{}' because work_dir '{}' matched.", path.to_string_lossy(), work_dir);
info!("Selected shortcut for update '{:?}' because work_dir '{:?}' matched.", path, work_dir);
paths.push((flag, properties));
}
} else {
warn!("Could not resolve target or work_dir for shortcut '{}'.", path.to_string_lossy());
warn!("Could not resolve target or work_dir for shortcut '{:?}'.", path);
}
}
Err(e) => {
@@ -341,7 +346,7 @@ unsafe fn unsafe_remove_all_shortcuts_for_root_dir<P: AsRef<Path>>(root_dir: P)
let shortcuts = unsafe_get_shortcuts_for_root_dir(root_dir);
for (flag, properties) in shortcuts {
let path = properties.get_link_path();
info!("Removing shortcut '{}' ({:?}).", path, flag);
info!("Removing shortcut '{:?}' ({:?}).", path, flag);
let remove_parent_if_empty = flag == ShortcutLocationFlags::START_MENU;
if let Err(e) = unsafe_delete_lnk_file(&path, remove_parent_if_empty) {
warn!("Failed to remove shortcut: {}", e);
@@ -383,8 +388,8 @@ unsafe fn unsafe_unpin_lnk_from_start<P: AsRef<Path>>(path: P) -> Result<()> {
return Ok(());
}
let path = string_to_u16(path.to_string_lossy());
let item_result: IShellItem = SHCreateItemFromParsingName(PCWSTR(path.as_ptr()), None)?;
let path = string_to_wide(path);
let item_result: IShellItem = SHCreateItemFromParsingName(path.as_pcwstr(), None)?;
let pinned_list: IStartMenuPinnedList = create_instance(&StartMenuPin)?;
pinned_list.RemoveFromList(&item_result)?;
Ok(())
@@ -422,89 +427,86 @@ where
struct Lnk {
me: IShellLinkW,
pf: IPersistFile,
my_path: String,
my_path: PathBuf,
}
#[allow(dead_code)]
impl Lnk {
pub unsafe fn get_link_path(&self) -> String {
pub unsafe fn get_link_path(&self) -> PathBuf {
self.my_path.clone()
}
pub unsafe fn get_arguments(&self) -> Result<String> {
let mut pszargs = [0u16; 1024];
self.me.GetArguments(&mut pszargs)?;
let args = u16_to_string(pszargs)?;
let args = wide_to_string(pszargs)?;
Ok(args)
}
pub unsafe fn get_description(&self) -> Result<String> {
let mut pszdesc = [0u16; 1024];
self.me.GetDescription(&mut pszdesc)?;
let desc = u16_to_string(pszdesc)?;
let desc = wide_to_string(pszdesc)?;
Ok(desc)
}
pub unsafe fn get_icon_location(&self) -> Result<(String, i32)> {
pub unsafe fn get_icon_location(&self) -> Result<(PathBuf, i32)> {
let mut pszfile = [0u16; 1024];
let mut pindex = 0;
self.me.GetIconLocation(&mut pszfile, &mut pindex)?;
let icon = u16_to_string(pszfile)?;
let icon = wide_to_os_string(pszfile);
let icon = PathBuf::from(icon);
Ok((icon, pindex))
}
pub unsafe fn get_target_path(&self) -> Result<String> {
pub unsafe fn get_target_path(&self) -> Result<PathBuf> {
let mut pszfile = [0u16; 1024];
self.me.GetPath(&mut pszfile, std::ptr::null_mut(), 0)?;
let target = u16_to_string(pszfile)?;
let target = wide_to_os_string(pszfile);
let target = PathBuf::from(target);
Ok(target)
}
pub unsafe fn get_working_directory(&self) -> Result<String> {
pub unsafe fn get_working_directory(&self) -> Result<PathBuf> {
let mut pszdir = [0u16; 1024];
self.me.GetWorkingDirectory(&mut pszdir)?;
let work_dir = u16_to_string(pszdir)?;
let work_dir = wide_to_os_string(pszdir);
let work_dir = PathBuf::from(work_dir);
Ok(work_dir)
}
pub unsafe fn set_arguments(&mut self, path: &str) -> Result<()> {
let args = string_to_u16(path);
let args = PCWSTR(args.as_ptr());
Ok(self.me.SetArguments(args)?)
pub unsafe fn set_arguments<P: AsRef<str>>(&mut self, path: P) -> Result<()> {
let args = string_to_wide(path.as_ref());
Ok(self.me.SetArguments(args.as_pcwstr())?)
}
pub unsafe fn set_description(&mut self, path: &str) -> Result<()> {
let desc = string_to_u16(path);
let desc = PCWSTR(desc.as_ptr());
Ok(self.me.SetDescription(desc)?)
pub unsafe fn set_description<P: AsRef<str>>(&mut self, path: P) -> Result<()> {
let desc = string_to_wide(path.as_ref());
Ok(self.me.SetDescription(desc.as_pcwstr())?)
}
pub unsafe fn set_icon_location(&mut self, path: &str, index: i32) -> Result<()> {
let icon = string_to_u16(path);
let icon = PCWSTR(icon.as_ptr());
Ok(self.me.SetIconLocation(icon, index)?)
pub unsafe fn set_icon_location<P: AsRef<Path>>(&mut self, path: P, index: i32) -> Result<()> {
let icon = string_to_wide(path.as_ref());
Ok(self.me.SetIconLocation(icon.as_pcwstr(), index)?)
}
pub unsafe fn set_target_path(&mut self, path: &str) -> Result<()> {
let target = string_to_u16(path);
let target = PCWSTR(target.as_ptr());
Ok(self.me.SetPath(target)?)
pub unsafe fn set_target_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let target = string_to_wide(path.as_ref());
Ok(self.me.SetPath(target.as_pcwstr())?)
}
pub unsafe fn set_working_directory(&mut self, path: &str) -> Result<()> {
let work_dir = string_to_u16(path);
let work_dir = PCWSTR(work_dir.as_ptr());
Ok(self.me.SetWorkingDirectory(work_dir)?)
pub unsafe fn set_working_directory<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let work_dir = string_to_wide(path.as_ref());
Ok(self.me.SetWorkingDirectory(work_dir.as_pcwstr())?)
}
pub unsafe fn set_aumid(&mut self, app_model_id: Option<&str>) -> Result<()> {
pub unsafe fn set_aumid<P: AsRef<str>>(&mut self, app_model_id: Option<P>) -> Result<()> {
// Set app user model ID property
// Docs: https://docs.microsoft.com/windows/win32/properties/props-system-appusermodel-id
let store: IPropertyStore = self.me.cast()?;
if let Some(app_model_id) = app_model_id {
let id = string_to_u16(app_model_id);
let id = PCWSTR(id.as_ptr());
let variant = InitPropVariantFromStringVector(Some(&[id]))?;
let id = string_to_wide(app_model_id.as_ref());
let variant = InitPropVariantFromStringVector(Some(&[id.as_pcwstr()]))?;
store.SetValue(&PKEY_AppUserModel_ID, &variant)?;
} else {
let prop_variant = PROPVARIANT::default(); // defaults to VT_EMPTY
@@ -515,26 +517,26 @@ impl Lnk {
}
pub unsafe fn save(&mut self) -> Result<()> {
if self.my_path.as_os_str().is_empty() {
return Err(anyhow!("Cannot save shortcut without a path."));
}
Ok(self.pf.Save(None, true)?)
}
pub unsafe fn save_as(&mut self, path: &str) -> Result<()> {
let output = string_to_u16(path);
let output = PCWSTR(output.as_ptr());
self.my_path = path.to_string();
Ok(self.pf.Save(output, true)?)
pub unsafe fn save_as<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let output = string_to_wide(path.as_ref());
self.my_path = path.as_ref().to_path_buf();
Ok(self.pf.Save(output.as_pcwstr(), true)?)
}
pub unsafe fn open_write<P: AsRef<Path>>(link_path: P) -> Result<Lnk> {
let link_path = link_path.as_ref().to_string_lossy().to_string();
let link_path = link_path.as_ref();
let link: IShellLinkW = create_instance(&ShellLink)?;
let persist: IPersistFile = link.cast()?;
debug!("Loading link: {}", link_path);
debug!("Loading link: {:?}", link_path);
let link_pcwstr = string_to_u16(&link_path);
let link_pcwstr = PCWSTR(link_pcwstr.as_ptr());
persist.Load(link_pcwstr, STGM_READWRITE)?;
let link_pcwstr = string_to_wide(link_path);
persist.Load(link_pcwstr.as_pcwstr(), STGM_READWRITE)?;
// we don't really want to "resolve" the shortcut in the middle of an update operation
// this can cause Windows to move the target path of a shortcut to one of our temp dirs etc
@@ -548,19 +550,19 @@ impl Lnk {
// warn!("Failed to resolve link {} ({:?})", link_path, e);
// }
Ok(Lnk { me: link, pf: persist, my_path: link_path })
Ok(Lnk { me: link, pf: persist, my_path: link_path.to_path_buf() })
}
pub unsafe fn create_new() -> Result<Lnk> {
let link: IShellLinkW = create_instance(&ShellLink)?;
let persist: IPersistFile = link.cast()?;
Ok(Lnk { me: link, pf: persist, my_path: String::new() })
Ok(Lnk { me: link, pf: persist, my_path: PathBuf::new() })
}
}
impl std::fmt::Debug for Lnk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Lnk({})", self.my_path)
write!(f, "Lnk({:?})", self.my_path)
}
}
@@ -626,7 +628,7 @@ fn test_can_resolve_existing_shortcut() {
unsafe {
unsafe_run_delegate_in_com_context(move || {
let l = Lnk::open_write(link_path).unwrap();
let target = l.get_target_path().unwrap();
let target = l.get_target_path().unwrap().to_string_lossy().to_string();
assert_eq!(target, "C:\\Users\\Caelan\\AppData\\Local\\Discord\\Update.exe");
Ok(())
})
@@ -696,8 +698,8 @@ fn test_shortcut_full_integration() {
assert_eq!(shortcuts[2].0, ShortcutLocationFlags::START_MENU);
assert_eq!(PathBuf::from(&shortcuts[2].1.my_path), link2);
assert_eq!(shortcuts[0].1.get_target_path().unwrap(), target);
assert_eq!(shortcuts[0].1.get_working_directory().unwrap(), work);
assert_eq!(shortcuts[0].1.get_target_path().unwrap().to_string_lossy().to_string(), target);
assert_eq!(shortcuts[0].1.get_working_directory().unwrap().to_string_lossy().to_string(), work);
unsafe_remove_all_shortcuts_for_root_dir(root);
assert!(!link1.exists());

View File

@@ -1,8 +1,8 @@
use super::strings::string_to_wide;
use anyhow::{bail, Result};
use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, ImageFormat, ImageReader};
use std::sync::mpsc::{self, Receiver, Sender};
use std::{io::Cursor, thread};
use velopack::wide_strings::string_to_wide;
use windows::{
core::HRESULT,
Win32::{

View File

@@ -1,149 +0,0 @@
use anyhow::Result;
use windows::core::{PCWSTR, PWSTR};
pub struct WideString {
inner: Vec<u16>,
}
impl WideString {
pub fn as_ptr(&self) -> *const u16 {
self.inner.as_ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut u16 {
self.inner.as_mut_ptr()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn as_pcwstr(&self) -> PCWSTR {
PCWSTR(self.as_ptr())
}
pub fn as_pwstr(&mut self) -> PWSTR {
PWSTR(self.as_mut_ptr())
}
}
impl Into<Vec<u16>> for WideString {
fn into(self) -> Vec<u16> {
self.inner
}
}
impl Into<PCWSTR> for WideString {
fn into(self) -> PCWSTR {
self.as_pcwstr()
}
}
impl AsRef<[u16]> for WideString {
fn as_ref(&self) -> &[u16] {
&self.inner
}
}
impl AsMut<[u16]> for WideString {
fn as_mut(&mut self) -> &mut [u16] {
&mut self.inner
}
}
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 string_to_wide<P: AsRef<str>>(input: P) -> WideString {
WideString { inner: string_to_u16(input) }
}
pub trait WideStringRef {
fn to_wide_slice(&self) -> &[u16];
}
impl WideStringRef for PWSTR {
fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() }
}
}
impl WideStringRef for PCWSTR {
fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() }
}
}
impl WideStringRef for Vec<u16> {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
impl WideStringRef for &Vec<u16> {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
// impl WideString for [u16] {
// fn to_wide_slice(&self) -> &[u16] {
// self.as_ref()
// }
// }
impl<const N: usize> WideStringRef for [u16; N] {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
pub fn u16_to_string_lossy<T: WideStringRef>(input: T) -> String {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
String::from_utf16_lossy(trimmed_slice)
}
pub fn u16_to_string<T: WideStringRef>(input: T) -> Result<String> {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
Ok(String::from_utf16(trimmed_slice)?)
}
// 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())
// }
// pub fn u16_to_string<T: AsRef<[u16]>>(input: T) -> Result<String> {
// let input = input.as_ref();
// // Find position of first null byte (0)
// let null_pos = input.iter().position(|&x| x == 0).unwrap_or(input.len());
// // Take only up to the first null byte
// let trimmed_input = &input[..null_pos];
// let hstring = HSTRING::from_wide(trimmed_input);
// Ok(hstring.to_string_lossy())
// }

View File

@@ -1,16 +1,17 @@
use crate::{
shared::{self, runtime_arch::RuntimeArch},
windows::strings::{string_to_u16, u16_to_string},
};
use crate::shared::{self, runtime_arch::RuntimeArch};
use anyhow::{anyhow, Result};
use normpath::PathExt;
use std::{
ffi::{OsStr, OsString},
path::{Path, PathBuf},
time::Duration,
};
use velopack::{locator::VelopackLocator, process, process::WaitResult};
use velopack::{
locator::VelopackLocator,
process::{self, WaitResult},
wide_strings::{string_to_wide, wide_to_os_string},
};
use windows::{
core::PCWSTR,
Win32::Storage::FileSystem::GetLongPathNameW,
Win32::System::SystemInformation::{VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_FLAGS},
};
@@ -21,17 +22,11 @@ pub fn run_hook(locator: &VelopackLocator, hook_name: &str, timeout_secs: u64) -
let current_path = locator.get_current_bin_dir();
let main_exe_path = locator.get_main_exe_path();
let ver_string = locator.get_manifest_version_full_string();
let args = vec![hook_name, &ver_string];
let args: Vec<OsString> = vec![hook_name.into(), ver_string.into()];
let mut success = false;
info!("Running {} hook...", hook_name);
let cmd = process::run_process(
main_exe_path,
args.iter().map(|f| f.to_string()).collect(),
Some(current_path.to_string_lossy().to_string()),
false,
None,
);
let cmd = process::run_process(main_exe_path, args, Some(current_path), false, None);
if let Err(e) = cmd {
warn!("Failed to start hook {}: {}", hook_name, e);
@@ -66,22 +61,22 @@ pub fn run_hook(locator: &VelopackLocator, hook_name: &str, timeout_secs: u64) -
success
}
pub fn expand_environment_strings<P: AsRef<str>>(input: P) -> Result<String> {
pub fn expand_environment_strings<P: AsRef<OsStr>>(input: P) -> Result<OsString> {
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) };
let input = input.as_ref();
let encoded = string_to_wide(input);
let mut buffer_size = unsafe { ExpandEnvironmentStringsW(encoded.as_pcwstr(), 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)) };
buffer_size = unsafe { ExpandEnvironmentStringsW(encoded.as_pcwstr(), Some(&mut buffer)) };
if buffer_size == 0 {
return Err(anyhow!(windows::core::Error::from_win32()));
}
super::strings::u16_to_string(buffer)
Ok(wide_to_os_string(buffer))
}
#[test]
@@ -91,24 +86,23 @@ fn test_expand_environment_strings() {
assert!(expand_environment_strings("%windir%\\system32\\").unwrap().eq_ignore_ascii_case("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());
pub fn get_long_path<P: AsRef<OsStr>>(str: P) -> Result<OsString> {
let str = str.as_ref();
let str = string_to_wide(str);
// SAFETY: str is a valid wide string, this call will return required size of buffer
let len = unsafe { GetLongPathNameW(str, None) };
let len = unsafe { GetLongPathNameW(str.as_pcwstr(), 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())) };
let len = unsafe { GetLongPathNameW(str.as_pcwstr(), Some(vec.as_mut_slice())) };
if len == 0 {
return Err(anyhow!(windows::core::Error::from_win32()));
}
let result = u16_to_string(vec)?;
Ok(result)
Ok(wide_to_os_string(vec))
}
pub fn is_directory_writable<P1: AsRef<Path>>(path: P1) -> bool {
@@ -165,21 +159,12 @@ pub fn is_sub_path<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, parent: P2) -> Re
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),
}
let mut path = get_long_path(&path).unwrap_or(path.into());
let mut parent = get_long_path(&parent).unwrap_or(parent.into());
path = path.to_lowercase();
parent = parent.to_lowercase();
path = path.to_ascii_lowercase();
parent = parent.to_ascii_lowercase();
let path = PathBuf::from(path);
let parent = PathBuf::from(parent);
@@ -309,7 +294,11 @@ pub fn is_os_version_or_greater(version: &str) -> Result<bool> {
}
if major == 8 {
return Ok(if minor >= 1 { is_windows_8_or_greater() } else { is_windows_8_1_or_greater() });
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

View File

@@ -4,6 +4,7 @@ use libc::{c_void, size_t};
use std::{
collections::HashMap,
ffi::CString,
path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::Sender,
@@ -43,7 +44,7 @@ impl UpdateSource for CCallbackUpdateSource {
if let Some(cb_get_release_feed) = self.cb_get_release_feed {
let json_cstr_ptr = (cb_get_release_feed)(self.p_user_data, releases_name_cstr.as_ptr());
let json = c_to_String(json_cstr_ptr).map_err(|_| {
Error::Generic("User vpkc_release_feed_delegate_t returned a null pointer instead of an asset feed".to_string())
Error::Other("User vpkc_release_feed_delegate_t returned a null pointer instead of an asset feed".to_string())
})?;
if let Some(cb_free_release_feed) = self.cb_free_release_feed {
(cb_free_release_feed)(self.p_user_data, json_cstr_ptr); // Free the C string returned by the callback
@@ -53,12 +54,13 @@ impl UpdateSource for CCallbackUpdateSource {
let feed: VelopackAssetFeed = serde_json::from_str(&json)?;
Ok(feed)
} else {
Err(Error::Generic("User vpkc_release_feed_delegate_t is null".to_string()))
Err(Error::Other("User vpkc_release_feed_delegate_t is null".to_string()))
}
}
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
if let Some(cb_download_release_entry) = self.cb_download_release_entry {
let local_file = local_file.to_string_lossy().to_string();
let local_file_cstr = CString::new(local_file).unwrap();
let asset_ptr = unsafe { allocate_VelopackAsset(asset) };
@@ -76,12 +78,12 @@ impl UpdateSource for CCallbackUpdateSource {
}
if !success {
return Err(Error::Generic("User vpkc_download_asset_delegate_t returned false to indicate download failed".to_owned()));
return Err(Error::Other("User vpkc_download_asset_delegate_t returned false to indicate download failed".to_owned()));
}
Ok(())
} else {
Err(Error::Generic("User vpkc_download_asset_delegate_t is null".to_string()))
Err(Error::Other("User vpkc_download_asset_delegate_t is null".to_string()))
}
}

View File

@@ -1,8 +1,8 @@
use anyhow::{bail, Result};
use libc::{c_char, c_void, size_t};
use std::ffi::{CStr, CString};
use std::path::PathBuf;
use std::mem::size_of;
use std::path::PathBuf;
use velopack::{locator::VelopackLocatorConfig, UpdateInfo, UpdateOptions, VelopackAsset};
/// The result of a call to check for updates. This can indicate that an update is available, or that an error occurred.

View File

@@ -1,5 +1,6 @@
#![allow(missing_docs)]
use std::io::Cursor;
use std::{
cell::RefCell,
fs::{self, File},
@@ -7,15 +8,14 @@ use std::{
path::{Path, PathBuf},
rc::Rc,
};
use std::io::Cursor;
use regex::Regex;
use semver::Version;
use xml::EventReader;
use xml::reader::XmlEvent;
use xml::EventReader;
use zip::ZipArchive;
use crate::{Error, misc};
use crate::{misc, Error};
#[cfg(target_os = "macos")]
use std::os::unix::fs::PermissionsExt;
@@ -38,14 +38,14 @@ pub struct BundleZip<'a> {
pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleZip<'a>, Error> {
let file_name = file_name.as_ref();
debug!("Loading bundle from file '{}'...", file_name.to_string_lossy());
debug!("Loading bundle from file '{:?}'...", file_name);
let file = misc::retry_io(|| File::open(&file_name))?;
let cursor: Box<dyn ReadSeek> = Box::new(file);
let zip = ZipArchive::new(cursor)?;
Ok(BundleZip {
zip: Rc::new(RefCell::new(zip)),
zip_from_file: true,
file_path: Some(file_name.to_owned()),
Ok(BundleZip {
zip: Rc::new(RefCell::new(zip)),
zip_from_file: true,
file_path: Some(file_name.to_owned()),
zip_range: None,
manifest: None,
})
@@ -55,13 +55,7 @@ pub fn load_bundle_from_memory(zip_range: &[u8]) -> Result<BundleZip, Error> {
info!("Loading bundle from embedded zip...");
let cursor: Box<dyn ReadSeek> = Box::new(Cursor::new(zip_range));
let zip = ZipArchive::new(cursor)?;
Ok(BundleZip {
zip: Rc::new(RefCell::new(zip)),
zip_from_file: false,
zip_range: Some(zip_range),
file_path: None,
manifest: None,
})
Ok(BundleZip { zip: Rc::new(RefCell::new(zip)), zip_from_file: false, zip_range: Some(zip_range), file_path: None, manifest: None })
}
#[allow(dead_code)]
@@ -75,7 +69,7 @@ impl BundleZip<'_> {
}
Ok(())
}
pub fn calculate_size(&self) -> (u64, u64) {
let mut total_uncompressed_size = 0u64;
let mut total_compressed_size = 0u64;
@@ -140,7 +134,7 @@ impl BundleZip<'_> {
pub fn extract_zip_idx_to_path<T: AsRef<Path>>(&self, index: usize, path: T) -> Result<(), Error> {
let path = path.as_ref();
debug!("Extracting zip file to path: {}", path.to_string_lossy());
debug!("Extracting zip file to path: {:?}", path);
let p = PathBuf::from(path);
let parent = p.parent().unwrap();
@@ -172,7 +166,7 @@ impl BundleZip<'_> {
{
let idx = self.find_zip_file(predicate);
if idx.is_none() {
return Err(Error::FileNotFound("(zip bundle predicate)".to_owned()));
return Err(Error::InvalidPackage("(zip bundle predicate not found)".to_owned()));
}
let idx = idx.unwrap();
self.extract_zip_idx_to_path(idx, path)?;
@@ -183,15 +177,15 @@ impl BundleZip<'_> {
if let Some(manifest) = &self.manifest {
return Ok(manifest.clone());
}
let nuspec_idx = self.find_zip_file(|name| name.ends_with(".nuspec"))
.ok_or(Error::MissingNuspec)?;
let nuspec_idx =
self.find_zip_file(|name| name.ends_with(".nuspec")).ok_or(Error::InvalidPackage("No .nuspec manifest found".into()))?;
let mut contents = String::new();
let mut archive = self.zip.borrow_mut();
archive.by_index(nuspec_idx)?.read_to_string(&mut contents)?;
let app = read_manifest_from_string(&contents)?;
self.manifest = Some(app.clone());
Ok(app)
}
@@ -218,19 +212,23 @@ impl BundleZip<'_> {
{
let absolute_path = link_path.parent().unwrap().join(&target_path);
trace!(
"Creating symlink '{}' -> '{}', target isfile={}, isdir={}, relative={}",
link_path.to_string_lossy(),
absolute_path.to_string_lossy(),
"Creating symlink '{:?}' -> '{:?}', target isfile={}, isdir={}, relative={:?}",
link_path,
absolute_path,
absolute_path.is_file(),
absolute_path.is_dir(),
target_path.to_string_lossy()
target_path
);
if absolute_path.is_file() {
std::os::windows::fs::symlink_file(target_path, link_path)?;
} else if absolute_path.is_dir() {
std::os::windows::fs::symlink_dir(target_path, link_path)?;
} else {
return Err(Error::Generic("Could not create symlink: target is not a file or directory.".to_owned()));
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Target path '{:?}' does not exist or is of an invalid type", absolute_path),
)
.into());
}
}
#[cfg(not(target_os = "windows"))]
@@ -246,7 +244,7 @@ impl BundleZip<'_> {
let files = self.get_file_names()?;
let num_files = files.len();
info!("Extracting {} app files to '{}'...", num_files, current_path.to_string_lossy());
info!("Extracting {} app files to '{:?}'...", num_files, current_path);
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
let symlink_regex = Regex::new(".__symlink$").unwrap();
@@ -259,7 +257,7 @@ impl BundleZip<'_> {
let nuspec_path = current_path.join("sq.version");
let _ = self
.extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path)
.map_err(|_| Error::MissingNuspec)?;
.map_err(|_| Error::InvalidPackage("No .nuspec manifest found".into()))?;
}
// we extract the symlinks after, because the target must exist.
@@ -294,16 +292,16 @@ impl BundleZip<'_> {
#[cfg(target_os = "windows")]
let file_path_on_disk = file_path_on_disk.as_path();
debug!(" {} Extracting '{}' to '{}'", i, key, file_path_on_disk.to_string_lossy());
debug!(" {} Extracting '{}' to '{:?}'", i, key, file_path_on_disk);
self.extract_zip_idx_to_path(i, &file_path_on_disk)?;
// on macos, we need to chmod +x the executable files
// for now, we just chmod 755 every file we extract. this is not great, ideally we
// for now, we just chmod 755 every file we extract. this is not great, ideally we
// will preserve the mode as it was when packaging. This will come in a future release.
#[cfg(target_os = "macos")]
{
if let Err(e) = std::fs::set_permissions(&file_path_on_disk, std::fs::Permissions::from_mode(0o755)) {
warn!("Failed to set mode 755 on '{}': {}", file_path_on_disk.to_string_lossy(), e);
warn!("Failed to set mode 755 on '{}': {:?}", file_path_on_disk, e);
}
}
@@ -316,7 +314,7 @@ impl BundleZip<'_> {
let mut file = archive.by_index(i)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
info!(" {} Creating symlink '{}' -> '{}'", i, link_path.to_string_lossy(), contents);
info!(" {} Creating symlink '{:?}' -> '{}'", i, link_path, contents);
let contents = contents.trim_end_matches('/');
#[cfg(target_os = "windows")]
@@ -418,16 +416,16 @@ pub fn read_manifest_from_string(xml: &str) -> Result<Manifest, Error> {
}
if obj.id.is_empty() {
return Err(Error::MissingNuspecProperty("id".to_owned()));
return Err(Error::InvalidPackage("Missing required manifest property: id".into()));
}
if obj.version == Version::new(0, 0, 0) {
return Err(Error::MissingNuspecProperty("version".to_owned()));
return Err(Error::InvalidPackage("Missing required manifest property: version".into()));
}
#[cfg(target_os = "windows")]
if obj.main_exe.is_empty() {
return Err(Error::MissingNuspecProperty("mainExe".to_owned()));
return Err(Error::InvalidPackage("Missing required manifest property: mainExe".into()));
}
if obj.title.is_empty() {
@@ -485,7 +483,11 @@ fn parse_package_file_name<T: AsRef<str>>(name: T) -> Option<EntryNameInfo> {
let mut entry = EntryNameInfo::default();
entry.is_delta = delta;
let name_and_ver = if full { ENTRY_SUFFIX_FULL.replace(name, "") } else { ENTRY_SUFFIX_DELTA.replace(name, "") };
let name_and_ver = if full {
ENTRY_SUFFIX_FULL.replace(name, "")
} else {
ENTRY_SUFFIX_DELTA.replace(name, "")
};
let ver_idx = ENTRY_VERSION_START.find(&name_and_ver);
if ver_idx.is_none() {
return None;
@@ -528,4 +530,4 @@ fn test_parse_package_file_name() {
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3.nupkg").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2-full.nupkg").is_none());
}
}

View File

@@ -1,10 +1,11 @@
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use crate::{misc, Error};
/// Downloads a file from a URL and writes it to a file while reporting progress from 0-100.
pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> Result<(), Error>
pub fn download_url_to_file<A>(url: &str, file_path: &Path, mut progress: A) -> Result<(), Error>
where
A: FnMut(i16),
{
@@ -74,7 +75,7 @@ fn test_download_file_reports_progress() {
let tmpfile = tempfile::NamedTempFile::new().unwrap();
let tmppath = tmpfile.path();
download_url_to_file(test_file, tmppath.to_str().unwrap(), |p| {
download_url_to_file(test_file, Path::new(tmppath), |p| {
assert!(p >= last_prog);
prog_count += 1;
last_prog = p;
@@ -117,11 +118,7 @@ fn test_interrupted_download() {
});
let tmpfile = tempfile::NamedTempFile::new().unwrap();
let result = download_url_to_file(
&format!("http://{}", addr),
tmpfile.path().to_str().unwrap(),
|_| {}
);
let result = download_url_to_file(&format!("http://{}", addr), tmpfile.path(), |_| {});
assert!(result.is_err(), "Download should fail due to connection interruption");
}
@@ -152,13 +149,9 @@ fn test_successful_download() {
});
let tmpfile = tempfile::NamedTempFile::new().unwrap();
let _ = download_url_to_file(
&format!("http://{}", addr),
tmpfile.path().to_str().unwrap(),
|_| {},
).unwrap();
let _ = download_url_to_file(&format!("http://{}", addr), tmpfile.path(), |_| {}).unwrap();
// Verify that the downloaded file has the expected size
let metadata = tmpfile.path().metadata().unwrap();
assert_eq!(metadata.len(), 10240, "Downloaded file size should match the expected content size");
}
}

View File

@@ -116,6 +116,8 @@ macro_rules! maybe_pub_os {
};
}
use std::path::PathBuf;
mod app;
pub use app::*;
@@ -128,6 +130,7 @@ pub mod locator;
/// Sources are abstractions for custom update sources (eg. url, local file, github releases, etc).
pub mod sources;
maybe_pub!(wide_strings);
maybe_pub!(download, bundle, constants, lockfile, logging, misc);
maybe_pub_os!(process, "process_win.rs", "process_unix.rs");
@@ -147,13 +150,13 @@ pub enum NetworkError {
#[allow(missing_docs)]
pub enum Error {
#[error("File does not exist: {0}")]
FileNotFound(String),
FileNotFound(PathBuf),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Checksum did not match for {0} (expected {1}, actual {2})")]
ChecksumInvalid(String, String, String),
ChecksumInvalid(PathBuf, String, String),
#[error("Size did not match for {0} (expected {1}, actual {2})")]
SizeInvalid(String, u64, u64),
SizeInvalid(PathBuf, u64, u64),
#[error("Zip error: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("Network error: {0}")]
@@ -162,16 +165,14 @@ pub enum Error {
Json(#[from] serde_json::Error),
#[error("Semver parse error: {0}")]
Semver(#[from] semver::Error),
#[error("This application is missing a package manifest (.nuspec) or it could not be parsed.")]
MissingNuspec,
#[error("This application is missing a required property in its package manifest: {0}")]
MissingNuspecProperty(String),
#[error("This application is missing an Update.exe/UpdateNix/UpdateMac binary.")]
MissingUpdateExe,
#[error("This update package is invalid: {0}.")]
InvalidPackage(String),
#[error("This application is not properly installed: {0}")]
NotInstalled(String),
#[error("Generic error: {0}")]
Generic(String),
#[error("This is not supported: {0}")]
NotSupported(String),
#[error("{0}")]
Other(String),
#[cfg(target_os = "windows")]
#[error("Win32 error: {0}")]
Win32(#[from] windows::core::Error),

View File

@@ -110,10 +110,10 @@ impl VelopackLocator {
/// Creates a new VelopackLocator from the given paths, trying to auto-detect the manifest.
pub fn new(config: &VelopackLocatorConfig) -> Result<VelopackLocator, Error> {
if !config.UpdateExePath.exists() {
return Err(Error::MissingUpdateExe);
return Err(Error::NotInstalled("Update.exe does not exist in the expected path".to_owned()));
}
if !config.ManifestPath.exists() {
return Err(Error::MissingNuspec);
return Err(Error::NotInstalled("Manifest file does not exist in the expected path".to_owned()));
}
let manifest = read_current_manifest(&config.ManifestPath)?;
@@ -152,11 +152,6 @@ impl VelopackLocator {
self.paths.PackagesDir.clone()
}
/// Returns the path to the current app's packages directory as a string.
pub fn get_packages_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.PackagesDir)
}
/// Returns the path to the ideal local nupkg path.
pub fn get_ideal_local_nupkg_path(&self, id: Option<&str>, version: Option<Version>) -> PathBuf {
let id = id.unwrap_or(&self.manifest.id);
@@ -164,11 +159,6 @@ impl VelopackLocator {
self.paths.RootAppDir.join("packages").join(format!("{}-{}-full.nupkg", id, version))
}
/// Returns the path to the ideal local nupkg path as a string.
pub fn get_ideal_local_nupkg_path_as_string(&self, id: Option<&str>, version: Option<Version>) -> String {
Self::path_as_string(&self.get_ideal_local_nupkg_path(id, version))
}
/// Returns the path to the current app temporary directory.
pub fn get_temp_dir_root(&self) -> PathBuf {
self.paths.PackagesDir.join("VelopackTemp")
@@ -179,51 +169,26 @@ impl VelopackLocator {
self.get_temp_dir_root().join("tmp_".to_string() + &misc::random_string(16))
}
/// Returns the path to the current app temporary directory as a string.
pub fn get_temp_dir_as_string(&self) -> String {
Self::path_as_string(&self.get_temp_dir_root())
}
/// Returns the root directory of the current app.
pub fn get_root_dir(&self) -> PathBuf {
self.paths.RootAppDir.clone()
}
/// Returns the root directory of the current app as a string.
pub fn get_root_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.RootAppDir)
}
/// Returns the path to the current app's Update.exe binary.
pub fn get_update_path(&self) -> PathBuf {
self.paths.UpdateExePath.clone()
}
/// Returns the path to the current app's Update.exe binary as a string.
pub fn get_update_path_as_string(&self) -> String {
Self::path_as_string(&self.paths.UpdateExePath)
}
/// Returns the path to the current app's main executable.
pub fn get_main_exe_path(&self) -> PathBuf {
self.paths.CurrentBinaryDir.join(&self.manifest.main_exe)
}
/// Returns the path to the current app's main executable as a string.
pub fn get_main_exe_path_as_string(&self) -> String {
Self::path_as_string(&self.get_main_exe_path())
}
/// Returns the path to the current app's user binary directory.
pub fn get_current_bin_dir(&self) -> PathBuf {
self.paths.CurrentBinaryDir.clone()
}
/// Returns the path to the current app's user binary directory as a string.
pub fn get_current_bin_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.CurrentBinaryDir)
}
/// Returns a clone of the current app's manifest.
pub fn get_manifest(&self) -> Manifest {
self.manifest.clone()
@@ -311,10 +276,6 @@ impl VelopackLocator {
Ok(lock_file)
}
fn path_as_string(path: &PathBuf) -> String {
path.to_string_lossy().to_string()
}
fn get_or_create_staged_user_id(&self) -> String {
let packages_dir = self.get_packages_dir();
let beta_id_path = packages_dir.join(".betaId");
@@ -535,24 +496,24 @@ fn read_current_manifest(nuspec_path: &PathBuf) -> Result<Manifest, Error> {
return bundle::read_manifest_from_string(&nuspec);
}
}
Err(Error::MissingNuspec)
Err(Error::NotInstalled(format!("Manifest file does not exist or is not readable: {:?}", nuspec_path)))
}
/// Returns the path and manifest of the latest full package in the given directory.
pub fn find_latest_full_package(packages_dir: &PathBuf) -> Option<(PathBuf, Manifest)> {
let packages_dir = packages_dir.to_string_lossy();
info!("Attempting to auto-detect package in: {}", packages_dir);
info!("Attempting to auto-detect package in: {:?}", packages_dir);
let mut package: Option<(PathBuf, Manifest)> = None;
let search_glob = format!("{}/*-full.nupkg", packages_dir);
if let Ok(paths) = glob::glob(search_glob.as_str()) {
for path in paths.into_iter().flatten() {
trace!("Checking package: '{}'", path.to_string_lossy());
trace!("Checking package: '{:?}'", path);
if let Ok(mut bun) = bundle::load_bundle_from_file(&path) {
if let Ok(mani) = bun.read_manifest() {
if package.is_none() || mani.version > package.clone()?.1.version {
info!("Found {}: '{}'", mani.version, path.to_string_lossy());
info!("Found {}: '{:?}'", mani.version, path);
package = Some((path, mani));
}
}

View File

@@ -1,6 +1,7 @@
use semver::Version;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::{fs, process::exit, sync::mpsc::Sender};
#[cfg(feature = "async")]
@@ -396,7 +397,7 @@ impl UpdateManager {
let partial_file = final_target_file.with_extension("partial");
if final_target_file.exists() {
info!("Package already exists on disk, skipping download: '{}'", final_target_file.to_string_lossy());
info!("Package already exists on disk, skipping download: '{:?}'", final_target_file);
return Ok(());
}
@@ -405,11 +406,11 @@ impl UpdateManager {
let delta_pattern = format!("{}/*-delta.nupkg", packages_dir.to_string_lossy());
let mut to_delete = Vec::new();
fn find_files_to_delete(pattern: &str, to_delete: &mut Vec<String>) {
fn find_files_to_delete(pattern: &str, to_delete: &mut Vec<PathBuf>) {
match glob::glob(pattern) {
Ok(paths) => {
for path in paths.into_iter().flatten() {
to_delete.push(path.to_string_lossy().to_string());
to_delete.push(path);
}
}
Err(e) => {
@@ -423,20 +424,19 @@ impl UpdateManager {
if update.BaseRelease.is_some() && !update.DeltasToTarget.is_empty() {
info!("Beginning delta update process.");
if let Err(e) = self.download_and_apply_delta_updates(update, &partial_file, progress.clone()) {
error!("Error downloading delta updates: {}", e);
if self.download_and_apply_delta_updates(update, &partial_file, progress.clone()).is_err() {
info!("Falling back to full update...");
self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?;
self.source.download_release_entry(&update.TargetFullRelease, &partial_file, progress)?;
self.verify_package_checksum(&partial_file, &update.TargetFullRelease)?;
info!("Successfully downloaded file: '{}'", partial_file.to_string_lossy());
info!("Successfully downloaded file: '{:?}'", partial_file);
}
} else {
self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?;
self.source.download_release_entry(&update.TargetFullRelease, &partial_file, progress)?;
self.verify_package_checksum(&partial_file, &update.TargetFullRelease)?;
info!("Successfully downloaded file: '{}'", partial_file.to_string_lossy());
info!("Successfully downloaded file: '{:?}'", partial_file);
}
info!("Renaming partial file to final target: '{}'", final_target_file.to_string_lossy());
info!("Renaming partial file to final target: '{:?}'", final_target_file);
fs::rename(&partial_file, &final_target_file)?;
find_files_to_delete(&delta_pattern, &mut to_delete);
@@ -457,7 +457,7 @@ impl UpdateManager {
}
for path in to_delete {
info!("Deleting up old package: '{}'", path);
info!("Deleting up old package: '{:?}'", path);
let _ = fs::remove_file(&path);
}
@@ -467,23 +467,21 @@ impl UpdateManager {
fn download_and_apply_delta_updates(
&self,
update: &UpdateInfo,
target_file: &PathBuf,
output_file: &PathBuf,
progress: Option<Sender<i16>>,
) -> Result<(), Error> {
let packages_dir = self.locator.get_packages_dir();
let base_release_path = packages_dir.join(&update.BaseRelease.as_ref().unwrap().FileName);
let base_release_path = base_release_path.to_string_lossy().to_string();
let output_path = target_file.to_string_lossy().to_string();
let mut args: Vec<String> =
["patch", "--old", &base_release_path, "--output", &output_path].iter().map(|s| s.to_string()).collect();
let mut args: Vec<OsString> =
vec!["patch".into(), "--old".into(), base_release_path.clone().into(), "--output".into(), output_file.clone().into()];
for (i, delta) in update.DeltasToTarget.iter().enumerate() {
let delta_file = packages_dir.join(&delta.FileName);
let partial_file = delta_file.with_extension("partial");
info!("Downloading delta package: '{}'", &delta.FileName);
self.source.download_release_entry(&delta, &partial_file.to_string_lossy(), None)?;
self.source.download_release_entry(&delta, &partial_file, None)?;
self.verify_package_checksum(&partial_file, delta)?;
fs::rename(&partial_file, &delta_file)?;
@@ -492,11 +490,11 @@ impl UpdateManager {
let _ = progress.send(((i as f64 / update.DeltasToTarget.len() as f64) * 70.0) as i16);
}
args.push("--delta".to_string());
args.push(delta_file.to_string_lossy().to_string());
args.push("--delta".into());
args.push(delta_file.into());
}
info!("Applying {} patches to {}.", update.DeltasToTarget.len(), output_path);
info!("Applying {} patches to {:?}.", update.DeltasToTarget.len(), output_file);
if let Some(progress) = &progress {
let _ = progress.send(70);
@@ -508,7 +506,7 @@ impl UpdateManager {
} else {
let error_message = String::from_utf8_lossy(&output.stderr);
error!("Error applying delta updates: {}", error_message);
return Err(Error::Generic(error_message.to_string()));
return Err(Error::Io(std::io::Error::new(std::io::ErrorKind::Other, "Process exited with non-zero status")));
}
if let Some(progress) = &progress {
@@ -517,17 +515,17 @@ impl UpdateManager {
Ok(())
}
fn verify_package_checksum(&self, file: &PathBuf, asset: &VelopackAsset) -> Result<(), Error> {
fn verify_package_checksum(&self, file: &Path, asset: &VelopackAsset) -> Result<(), Error> {
let file_size = file.metadata()?.len();
if file_size != asset.Size {
error!("File size mismatch for file '{}': expected {}, got {}", file.to_string_lossy(), asset.Size, file_size);
return Err(Error::SizeInvalid(file.to_string_lossy().to_string(), asset.Size, file_size));
error!("File size mismatch for file '{:?}': expected {}, got {}", file, asset.Size, file_size);
return Err(Error::SizeInvalid(file.to_path_buf(), asset.Size, file_size));
}
let (sha1, _) = misc::calculate_sha1_sha256(file)?;
if !sha1.eq_ignore_ascii_case(&asset.SHA1) {
error!("SHA1 checksum mismatch for file '{}': expected '{}', got '{}'", file.to_string_lossy(), asset.SHA1, sha1);
return Err(Error::ChecksumInvalid(file.to_string_lossy().to_string(), asset.SHA1.clone(), sha1));
error!("SHA1 checksum mismatch for file '{:?}': expected '{}', got '{}'", file, asset.SHA1, sha1);
return Err(Error::ChecksumInvalid(file.to_path_buf(), asset.SHA1.clone(), sha1));
}
Ok(())
}
@@ -577,7 +575,7 @@ impl UpdateManager {
pub fn apply_updates_and_restart_with_args<A, C, S>(&self, to_apply: A, restart_args: C) -> Result<(), Error>
where
A: AsRef<VelopackAsset>,
S: AsRef<str>,
S: AsRef<OsStr>,
C: IntoIterator<Item = S>,
{
self.wait_exit_then_apply_updates(to_apply, false, true, restart_args)?;
@@ -602,7 +600,7 @@ impl UpdateManager {
pub fn wait_exit_then_apply_updates<A, C, S>(&self, to_apply: A, silent: bool, restart: bool, restart_args: C) -> Result<(), Error>
where
A: AsRef<VelopackAsset>,
S: AsRef<str>,
S: AsRef<OsStr>,
C: IntoIterator<Item = S>,
{
self.unsafe_apply_updates(to_apply, silent, ApplyWaitMode::WaitCurrentProcess, restart, restart_args)?;
@@ -622,50 +620,49 @@ impl UpdateManager {
) -> Result<(), Error>
where
A: AsRef<VelopackAsset>,
S: AsRef<str>,
S: AsRef<OsStr>,
C: IntoIterator<Item = S>,
{
let to_apply = to_apply.as_ref();
let pkg_path = self.locator.get_packages_dir().join(&to_apply.FileName);
let pkg_path_str = pkg_path.to_string_lossy();
let mut args = Vec::new();
args.push("apply".to_string());
args.push("--package".to_string());
args.push(pkg_path_str.to_string());
if !pkg_path.exists() {
error!("Package does not exist on disk: '{}'", &pkg_path_str);
return Err(Error::FileNotFound(pkg_path_str.to_string()));
error!("Package does not exist on disk: '{:?}'", &pkg_path);
return Err(Error::FileNotFound(pkg_path));
}
let mut args: Vec<OsString> = Vec::new();
args.push("apply".into());
args.push("--package".into());
args.push(pkg_path.into());
match wait_mode {
ApplyWaitMode::NoWait => {}
ApplyWaitMode::WaitCurrentProcess => {
args.push("--waitPid".to_string());
args.push(format!("{}", std::process::id()));
args.push("--waitPid".into());
args.push(format!("{}", std::process::id()).into());
}
ApplyWaitMode::WaitPid(pid) => {
args.push("--waitPid".to_string());
args.push(format!("{}", pid));
args.push("--waitPid".into());
args.push(format!("{}", pid).into());
}
}
if silent {
args.push("--silent".to_string());
args.push("--silent".into());
}
if !restart {
args.push("--norestart".to_string());
args.push("--norestart".into());
}
args.push("--root".to_string());
args.push(self.locator.get_root_dir_as_string());
args.push("--root".into());
args.push(self.locator.get_root_dir().into());
let restart_args: Vec<String> = restart_args.into_iter().map(|item| item.as_ref().to_string()).collect();
let restart_args: Vec<OsString> = restart_args.into_iter().map(|item| item.as_ref().to_os_string()).collect();
if !restart_args.is_empty() {
args.push("--".to_string());
args.push("--".into());
for arg in restart_args {
args.push(arg);
}

View File

@@ -1,8 +1,9 @@
use crate::wide_strings::*;
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult},
os::{raw::c_void, windows::ffi::OsStrExt},
os::windows::ffi::OsStrExt,
path::Path,
time::Duration,
};
@@ -101,7 +102,7 @@ fn append_arg(cmd: &mut Vec<u16>, arg: &Arg, force_quotes: bool) -> IoResult<()>
Ok(())
}
fn make_command_line(argv0: Option<&OsStr>, args: &[Arg], force_quotes: bool) -> IoResult<Vec<u16>> {
fn make_command_line(argv0: Option<&OsStr>, args: &[Arg], force_quotes: bool) -> IoResult<WideString> {
// Encode the command and arguments in a command line string such
// that the spawned process may recover them using CommandLineToArgvW.
let mut cmd: Vec<u16> = Vec::new();
@@ -123,10 +124,10 @@ fn make_command_line(argv0: Option<&OsStr>, args: &[Arg], force_quotes: bool) ->
}
cmd.push(0);
Ok(cmd)
Ok(cmd.into())
}
fn make_envp(maybe_env: Option<HashMap<String, String>>) -> IoResult<(Option<*const c_void>, Vec<u16>)> {
fn make_envp(maybe_env: Option<HashMap<String, String>>) -> IoResult<Option<WideString>> {
// On Windows we pass an "environment block" which is not a char**, but
// rather a concatenation of null-terminated k=v\0 sequences, with a final
// \0 to terminate.
@@ -148,9 +149,9 @@ fn make_envp(maybe_env: Option<HashMap<String, String>>) -> IoResult<(Option<*co
blk.push(0);
}
blk.push(0);
Ok((Some(blk.as_ptr() as *mut c_void), blk))
Ok(Some(blk.into()))
} else {
Ok((None, Vec::new()))
Ok(None)
}
}
@@ -232,36 +233,18 @@ impl AsRef<HANDLE> for SafeProcessHandle {
// }
// }
fn os_to_pcwstr<P: AsRef<OsStr>>(d: P) -> IoResult<(PCWSTR, Vec<u16>)> {
let d = d.as_ref();
let d = OsString::from(d);
let mut d_str: Vec<u16> = ensure_no_nuls(d)?.encode_wide().collect();
d_str.push(0);
Ok((PCWSTR(d_str.as_ptr()), d_str))
}
fn pathopt_to_pcwstr<P: AsRef<Path>>(d: Option<P>) -> IoResult<(PCWSTR, Vec<u16>)> {
match d {
Some(dir) => {
let dir = dir.as_ref();
os_to_pcwstr(dir)
}
None => Ok((PCWSTR::null(), Vec::new())),
}
}
pub fn run_process_as_admin<P1: AsRef<Path>, P2: AsRef<Path>>(
exe_path: P1,
args: Vec<String>,
args: Vec<OsString>,
work_dir: Option<P2>,
show_window: bool,
) -> IoResult<SafeProcessHandle> {
let verb = os_to_pcwstr("runas")?;
let exe = os_to_pcwstr(exe_path.as_ref())?;
let verb = string_to_wide("runas");
let exe = string_to_wide(exe_path.as_ref());
let wrapped_args: Vec<Arg> = args.iter().map(|a| Arg::Regular(a.into())).collect();
let params = make_command_line(None, &wrapped_args, false)?;
let params = PCWSTR(params.as_ptr());
let work_dir = pathopt_to_pcwstr(work_dir.as_ref())?;
let work_dir = string_to_wide_opt(work_dir.map(|w| w.as_ref().to_path_buf()));
let n_show = if show_window {
windows::Win32::UI::WindowsAndMessaging::SW_NORMAL.0
@@ -272,10 +255,10 @@ pub fn run_process_as_admin<P1: AsRef<Path>, P2: AsRef<Path>>(
let mut exe_info: SHELLEXECUTEINFOW = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
fMask: SEE_MASK_NOCLOSEPROCESS,
lpVerb: verb.0,
lpFile: exe.0,
lpVerb: verb.as_pcwstr(),
lpFile: exe.as_pcwstr(),
lpParameters: params,
lpDirectory: work_dir.0,
lpDirectory: work_dir.as_ref().map(|d| d.as_pcwstr()).unwrap_or(PCWSTR::default()),
nShow: n_show,
..Default::default()
};
@@ -291,20 +274,16 @@ pub fn run_process_as_admin<P1: AsRef<Path>, P2: AsRef<Path>>(
pub fn run_process<P1: AsRef<Path>, P2: AsRef<Path>>(
exe_path: P1,
args: Vec<String>,
args: Vec<OsString>,
work_dir: Option<P2>,
show_window: bool,
set_env: Option<HashMap<String, String>>,
) -> IoResult<SafeProcessHandle> {
let exe_path = exe_path.as_ref();
let exe_path = OsString::from(exe_path);
let exe_name_ptr = os_to_pcwstr(&exe_path)?;
let work_dir = work_dir.map(|d| d.as_ref().to_path_buf());
let exe_path = string_to_wide(exe_path.as_ref());
let dirp = string_to_wide_opt(work_dir.map(|w| w.as_ref().to_path_buf()));
let envp = make_envp(set_env)?;
let wrapped_args: Vec<Arg> = args.iter().map(|a| Arg::Regular(a.into())).collect();
let mut params = make_command_line(Some(&exe_path), &wrapped_args, false)?;
let params = PWSTR(params.as_mut_ptr());
let mut params = make_command_line(Some(exe_path.as_os_str()), &wrapped_args, false)?;
let mut pi = windows::Win32::System::Threading::PROCESS_INFORMATION::default();
@@ -329,9 +308,6 @@ pub fn run_process<P1: AsRef<Path>, P2: AsRef<Path>>(
hStdError: HANDLE(std::ptr::null_mut()),
};
let envp = make_envp(set_env)?;
let dirp = pathopt_to_pcwstr(work_dir.as_deref())?;
let flags = if show_window {
CREATE_UNICODE_ENVIRONMENT
} else {
@@ -339,8 +315,19 @@ pub fn run_process<P1: AsRef<Path>, P2: AsRef<Path>>(
};
unsafe {
info!("About to launch: '{:?}' in dir '{:?}' with arguments: {:?}", exe_path, work_dir, args);
CreateProcessW(exe_name_ptr.0, Option::Some(params), None, None, false, flags, envp.0, dirp.0, &si, &mut pi)?;
info!("About to launch: '{:?}' in dir '{:?}' with arguments: {:?}", exe_path, dirp, args);
CreateProcessW(
exe_path.as_pcwstr(),
Some(params.as_pwstr()),
None,
None,
false,
flags,
envp.map(|e| e.as_cvoid()),
dirp.map(|d| d.as_pcwstr()).unwrap_or(PCWSTR::default()),
&si,
&mut pi,
)?;
let _ = AllowSetForegroundWindow(pi.dwProcessId);
let _ = CloseHandle(pi.hThread);
}

View File

@@ -3,8 +3,8 @@ use std::{
sync::mpsc::Sender,
};
use crate::*;
use crate::bundle::Manifest;
use crate::*;
/// Abstraction for finding and downloading updates from a package source / repository.
/// An implementation may copy a file from a local repository, download from a web address,
@@ -14,7 +14,7 @@ pub trait UpdateSource: Send + Sync {
/// can subsequently be downloaded with download_release_entry.
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest, staged_user_id: &str) -> Result<VelopackAssetFeed, Error>;
/// Download the specified VelopackAsset to the provided local file path.
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error>;
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option<Sender<i16>>) -> Result<(), Error>;
/// Clone the source to create a new lifetime.
fn clone_boxed(&self) -> Box<dyn UpdateSource>;
}
@@ -25,16 +25,21 @@ impl Clone for Box<dyn UpdateSource> {
}
}
/// A source that does not provide any update capability.
/// A source that does not provide any update capability.
#[derive(Clone)]
pub struct NoneSource {}
impl UpdateSource for NoneSource {
fn get_release_feed(&self, _channel: &str, _app: &Manifest, _staged_user_id: &str) -> Result<VelopackAssetFeed, Error> {
Err(Error::Generic("None source does not checking release feed".to_owned()))
Err(Error::NotSupported("None source does not checking release feed".to_owned()))
}
fn download_release_entry(&self, _asset: &VelopackAsset, _local_file: &str, _progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
Err(Error::Generic("None source does not support downloads".to_owned()))
fn download_release_entry(
&self,
_asset: &VelopackAsset,
_local_file: &Path,
_progress_sender: Option<Sender<i16>>,
) -> Result<(), Error> {
Err(Error::NotSupported("None source does not support downloads".to_owned()))
}
fn clone_boxed(&self) -> Box<dyn UpdateSource> {
Box::new(self.clone())
@@ -72,7 +77,7 @@ impl UpdateSource for AutoSource {
self.source.get_release_feed(channel, app, staged_user_id)
}
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
self.source.download_release_entry(asset, local_file, progress_sender)
}
@@ -111,12 +116,12 @@ impl UpdateSource for HttpSource {
Ok(feed)
}
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
let path = self.url.trim_end_matches('/').to_owned() + "/";
let url = url::Url::parse(&path)?;
let asset_url = url.join(&asset.FileName)?;
info!("About to download from URL '{}' to file '{}'", asset_url, local_file);
info!("About to download from URL '{}' to file '{:?}'", asset_url, local_file);
download::download_url_to_file(asset_url.as_str(), local_file, move |p| {
if let Some(progress_sender) = &progress_sender {
let _ = progress_sender.send(p);
@@ -150,15 +155,15 @@ impl UpdateSource for FileSource {
let releases_name = format!("releases.{}.json", channel);
let releases_path = self.path.join(&releases_name);
info!("Reading releases from file: {}", releases_path.display());
info!("Reading releases from file: {:?}", releases_path);
let json = std::fs::read_to_string(releases_path)?;
let feed: VelopackAssetFeed = serde_json::from_str(&json)?;
Ok(feed)
}
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &Path, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
let asset_path = self.path.join(&asset.FileName);
info!("About to copy from file '{}' to file '{}'", asset_path.display(), local_file);
info!("About to copy from file '{:?}' to file '{:?}'", asset_path, local_file);
if let Some(progress_sender) = &progress_sender {
let _ = progress_sender.send(50);
}

View File

@@ -0,0 +1,190 @@
use std::{
ffi::{c_void, OsStr, OsString},
fmt::Debug,
os::windows::ffi::{OsStrExt, OsStringExt},
};
use windows::core::{PCWSTR, PWSTR};
#[derive(Clone, PartialEq, Eq)]
pub struct WideString {
str: OsString,
vec: Vec<u16>,
}
impl WideString {
pub fn as_slice(&self) -> &[u16] {
&self.vec
}
pub fn as_mut_slice(&mut self) -> &mut [u16] {
&mut self.vec
}
pub fn len(&self) -> usize {
self.vec.len()
}
pub fn as_os_str(&self) -> &OsStr {
self.str.as_os_str()
}
pub fn as_cvoid(&self) -> *const c_void {
self.as_ptr() as *const c_void
}
pub fn as_mut_cvoid(&mut self) -> *const c_void {
self.as_mut_ptr() as *const c_void
}
pub fn as_ptr(&self) -> *const u16 {
self.as_slice().as_ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut u16 {
self.as_mut_slice().as_mut_ptr()
}
pub fn as_pcwstr(&self) -> PCWSTR {
PCWSTR(self.as_ptr())
}
pub fn as_pwstr(&mut self) -> PWSTR {
PWSTR(self.as_mut_ptr())
}
}
impl Debug for WideString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WideString({:?})", self.str)
}
}
impl From<Vec<u16>> for WideString {
fn from(inner: Vec<u16>) -> Self {
WideString { str: OsString::from_wide(&inner), vec: inner }
}
}
impl From<String> for WideString {
fn from(inner: String) -> Self {
string_to_wide(inner)
}
}
impl From<&str> for WideString {
fn from(inner: &str) -> Self {
string_to_wide(inner)
}
}
impl Into<Vec<u16>> for WideString {
fn into(self) -> Vec<u16> {
self.vec
}
}
impl AsRef<OsStr> for WideString {
fn as_ref(&self) -> &OsStr {
self.str.as_os_str()
}
}
impl AsRef<[u16]> for WideString {
fn as_ref(&self) -> &[u16] {
&self.vec
}
}
impl AsMut<[u16]> for WideString {
fn as_mut(&mut self) -> &mut [u16] {
&mut self.vec
}
}
pub fn string_to_wide<P: AsRef<OsStr>>(input: P) -> WideString {
let str = input.as_ref();
let str = OsString::from(str);
let vec = str
.encode_wide()
.filter(|f| *f != 0) // Filter out any null characters
.chain(Some(0))
.collect::<Vec<u16>>();
WideString { vec, str }
}
pub fn string_to_wide_opt<P: AsRef<OsStr>>(input: Option<P>) -> Option<WideString> {
input.map(string_to_wide)
}
pub trait ToWideSlice {
fn to_wide_slice(&self) -> &[u16];
}
impl ToWideSlice for PWSTR {
fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() }
}
}
impl ToWideSlice for PCWSTR {
fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() }
}
}
impl ToWideSlice for Vec<u16> {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
impl ToWideSlice for &Vec<u16> {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
impl ToWideSlice for &[u16] {
fn to_wide_slice(&self) -> &[u16] {
self
}
}
impl<const N: usize> ToWideSlice for [u16; N] {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
pub fn wide_to_string_lossy<T: ToWideSlice>(input: T) -> String {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
String::from_utf16_lossy(trimmed_slice)
}
pub fn wide_to_string_lossy_opt<T: ToWideSlice>(input: Option<T>) -> Option<String> {
input.map(wide_to_string_lossy)
}
pub fn wide_to_string<T: ToWideSlice>(input: T) -> Result<String, std::string::FromUtf16Error> {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
Ok(String::from_utf16(trimmed_slice)?)
}
pub fn wide_to_string_opt<T: ToWideSlice>(input: Option<T>) -> Option<Result<String, std::string::FromUtf16Error>> {
input.map(wide_to_string)
}
pub fn wide_to_os_string<T: ToWideSlice>(input: T) -> OsString {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
OsString::from_wide(trimmed_slice)
}
pub fn wide_to_os_string_opt<T: ToWideSlice>(input: Option<T>) -> Option<OsString> {
input.map(wide_to_os_string)
}