mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Consolidate duplicate rust code (bins & lib-rust)
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -2051,19 +2051,20 @@ checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "9.0.1"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c"
|
||||
checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "9.0.1"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
|
||||
checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2140,10 +2141,14 @@ name = "velopack"
|
||||
version = "0.0.0-local"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"bitflags 2.6.0",
|
||||
"derivative",
|
||||
"glob",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"native-tls",
|
||||
"normpath",
|
||||
"regex",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2200,15 +2205,14 @@ dependencies = [
|
||||
"time 0.3.36",
|
||||
"ureq",
|
||||
"url",
|
||||
"velopack",
|
||||
"wait-timeout",
|
||||
"waitpid-any",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"winres",
|
||||
"winsafe",
|
||||
"xml",
|
||||
"zip",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -31,15 +31,14 @@ name = "testapp"
|
||||
path = "src/testapp.rs"
|
||||
|
||||
[dependencies]
|
||||
velopack = { path = "../lib-rust" }
|
||||
anyhow = "1.0"
|
||||
pretty-bytes-rust = "0.3"
|
||||
zip = { version = "2.1", default-features = false, features = ["deflate"] }
|
||||
regex = "1.10"
|
||||
rand = "0.8"
|
||||
log = "0.4"
|
||||
simplelog = "0.12"
|
||||
clap = "4.5"
|
||||
xml = "0.8"
|
||||
semver = "1.0"
|
||||
chrono = "0.4"
|
||||
wait-timeout = "0.2"
|
||||
@@ -58,7 +57,6 @@ enum-flags = "0.3"
|
||||
remove_dir_all = { git = "https://github.com/caesay/remove_dir_all.git", features = [
|
||||
"log",
|
||||
] }
|
||||
zstd = "0.13"
|
||||
sha1_smol = "1.0"
|
||||
url = "2.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -66,6 +64,7 @@ serde_json = "1.0"
|
||||
time = "0.3"
|
||||
os_info = "3.8"
|
||||
bitflags = "2.6"
|
||||
regex = "1.10"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
native-dialog = "0.7"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
bundle,
|
||||
shared::{self, bundle::Manifest, OperationWait},
|
||||
shared::{self, OperationWait},
|
||||
};
|
||||
use velopack::{bundle::load_bundle_from_file, bundle::Manifest, locator::VelopackLocator};
|
||||
use anyhow::{bail, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -13,26 +13,29 @@ use super::apply_osx_impl::apply_package_impl;
|
||||
use super::apply_windows_impl::apply_package_impl;
|
||||
|
||||
pub fn apply<'a>(
|
||||
root_path: &PathBuf,
|
||||
app: &Manifest,
|
||||
locator: &VelopackLocator,
|
||||
restart: bool,
|
||||
wait: OperationWait,
|
||||
package: Option<&PathBuf>,
|
||||
exe_args: Option<Vec<&str>>,
|
||||
runhooks: bool,
|
||||
run_hooks: bool,
|
||||
) -> Result<()> {
|
||||
shared::operation_wait(wait);
|
||||
|
||||
let package = package.cloned().map_or_else(|| auto_locate_package(&app, &root_path), Ok);
|
||||
let package = package.cloned().map_or_else(|| auto_locate_package(&locator), Ok);
|
||||
|
||||
match package {
|
||||
Ok(package) => {
|
||||
info!("Getting ready to apply package to {} ver {}: {}", app.id, app.version, package.to_string_lossy());
|
||||
match apply_package_impl(&root_path, &app, &package, runhooks) {
|
||||
Ok(applied_app) => {
|
||||
info!("Package version {} applied successfully.", applied_app.version);
|
||||
info!("Getting ready to apply package to {} ver {}: {}",
|
||||
locator.get_manifest_id(),
|
||||
locator.get_manifest_version_full_string(),
|
||||
package.to_string_lossy());
|
||||
match apply_package_impl(&locator, &package, run_hooks) {
|
||||
Ok(applied_locator) => {
|
||||
info!("Package version {} applied successfully.", applied_locator.get_manifest_version_full_string());
|
||||
// if successful, we want to restart the new version of the app, which could have different metadata
|
||||
if restart {
|
||||
shared::start_package(&applied_app, &root_path, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
shared::start_package(&applied_locator, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -48,20 +51,14 @@ pub fn apply<'a>(
|
||||
|
||||
// an error occurred if we're here, but we still want to restart the old version of the app if it was requested
|
||||
if restart {
|
||||
shared::start_package(&app, &root_path, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
shared::start_package(&locator, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
}
|
||||
|
||||
bail!("Apply failed, see logs for details.");
|
||||
}
|
||||
|
||||
fn auto_locate_package(app: &Manifest, _root_path: &PathBuf) -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let packages_dir = app.get_packages_path(_root_path);
|
||||
#[cfg(target_os = "linux")]
|
||||
let packages_dir = format!("/var/tmp/velopack/{}/packages", &app.id);
|
||||
#[cfg(target_os = "macos")]
|
||||
let packages_dir = format!("/tmp/velopack/{}/packages", &app.id);
|
||||
|
||||
fn auto_locate_package(locator: &VelopackLocator) -> Result<PathBuf> {
|
||||
let packages_dir = locator.get_packages_dir_as_string();
|
||||
info!("Attempting to auto-detect package in: {}", packages_dir);
|
||||
let mut package_path: Option<PathBuf> = None;
|
||||
let mut package_manifest: Option<Manifest> = None;
|
||||
@@ -70,7 +67,7 @@ fn auto_locate_package(app: &Manifest, _root_path: &PathBuf) -> Result<PathBuf>
|
||||
for path in paths {
|
||||
if let Ok(path) = path {
|
||||
trace!("Checking package: '{}'", path.to_string_lossy());
|
||||
if let Ok(bun) = bundle::load_bundle_from_file(&path) {
|
||||
if let Ok(mut bun) = load_bundle_from_file(&path) {
|
||||
if let Ok(mani) = bun.read_manifest() {
|
||||
if package_manifest.is_none() || mani.version > package_manifest.clone().unwrap().version {
|
||||
info!("Found {}: '{}'", mani.version, path.to_string_lossy());
|
||||
@@ -84,7 +81,7 @@ fn auto_locate_package(app: &Manifest, _root_path: &PathBuf) -> Result<PathBuf>
|
||||
}
|
||||
|
||||
if let Some(p) = package_path {
|
||||
return Ok(p);
|
||||
Ok(p)
|
||||
} else {
|
||||
bail!("Unable to find/load suitable package. Provide via the --package argument.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
dialogs,
|
||||
shared::{self, bundle, bundle::Manifest},
|
||||
shared::{self},
|
||||
windows::locksmith,
|
||||
windows::splash,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use velopack::{bundle::load_bundle_from_file, locator::VelopackLocator};
|
||||
|
||||
fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<()> {
|
||||
let source = source.as_ref();
|
||||
@@ -41,28 +42,31 @@ fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &PathBuf, run_hooks: bool) -> Result<Manifest> {
|
||||
let bundle = bundle::load_bundle_from_file(package)?;
|
||||
let new_app = bundle.read_manifest()?;
|
||||
pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_hooks: bool) -> Result<VelopackLocator> {
|
||||
let mut bundle = load_bundle_from_file(package)?;
|
||||
let new_app_manifest = bundle.read_manifest()?;
|
||||
let new_locator = old_locator.clone_self_with_new_manifest(&new_app_manifest);
|
||||
|
||||
let found_version = (new_app.version).to_owned();
|
||||
info!("Applying package to current: {} (old version {})", found_version, old_app.version);
|
||||
let root_path = old_locator.get_root_dir();
|
||||
let old_version = old_locator.get_manifest_version();
|
||||
let new_version = new_locator.get_manifest_version();
|
||||
|
||||
if !crate::windows::prerequisite::prompt_and_install_all_missing(&new_app, Some(&old_app.version))? {
|
||||
info!("Applying package {} to current: {}", new_version, old_version);
|
||||
|
||||
if !crate::windows::prerequisite::prompt_and_install_all_missing(&new_app_manifest, Some(&old_version))? {
|
||||
bail!("Stopping apply. Pre-requisites are missing and user cancelled.");
|
||||
}
|
||||
|
||||
let packages_dir = old_app.get_packages_path(root_path);
|
||||
let packages_dir = Path::new(&packages_dir);
|
||||
let current_dir = old_app.get_current_path(root_path);
|
||||
let packages_dir = old_locator.get_packages_dir();
|
||||
let current_dir = old_locator.get_current_bin_dir();
|
||||
let temp_path_new = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
|
||||
let temp_path_old = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
|
||||
|
||||
// open a dialog showing progress...
|
||||
let (mut tx, _) = mpsc::channel::<i16>();
|
||||
if !dialogs::get_silent() {
|
||||
let title = format!("{} Update", &new_app.title);
|
||||
let message = format!("Installing update {}...", &new_app.version);
|
||||
let title = format!("{} Update", new_locator.get_manifest_title());
|
||||
let message = format!("Installing update {}...", new_locator.get_manifest_version_full_string());
|
||||
tx = splash::show_progress_dialog(title, message);
|
||||
}
|
||||
|
||||
@@ -77,14 +81,14 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
|
||||
|
||||
// second, run application hooks (but don't care if it fails)
|
||||
if run_hooks {
|
||||
crate::windows::run_hook(old_app, root_path, "--veloapp-obsolete", 15);
|
||||
crate::windows::run_hook(old_locator, "--veloapp-obsolete", 15);
|
||||
} else {
|
||||
info!("Skipping --veloapp-obsolete hook.");
|
||||
}
|
||||
|
||||
// third, we try _REALLY HARD_ to stop the package
|
||||
let _ = shared::force_stop_package(root_path);
|
||||
if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&new_app, ¤t_dir) {
|
||||
if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&old_locator) {
|
||||
bail!("Failed to close processes locking directory / user cancelled.");
|
||||
}
|
||||
|
||||
@@ -116,10 +120,12 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
|
||||
let _ = tx.send(splash::MSG_CLOSE);
|
||||
|
||||
info!("Showing error dialog...");
|
||||
let title = format!("{} Update", &new_app.title);
|
||||
let title = format!("{} Update", &new_locator.get_manifest_title());
|
||||
let header = "Failed to update";
|
||||
let body =
|
||||
format!("Failed to update {} to version {}. Please check the logs for more details.", &new_app.title, &new_app.version);
|
||||
format!("Failed to update {} to version {}. Please check the logs for more details.",
|
||||
&new_locator.get_manifest_title(),
|
||||
&new_locator.get_manifest_version_full_string());
|
||||
dialogs::show_error(&title, Some(header), &body);
|
||||
|
||||
bail!("Fatal error performing update.");
|
||||
@@ -128,13 +134,23 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
|
||||
|
||||
// from this point on, we're past the point of no return and should not bail
|
||||
// sixth, we write the uninstall entry
|
||||
if let Err(e) = new_app.write_uninstall_entry(root_path) {
|
||||
warn!("Failed to write uninstall entry ({}).", e);
|
||||
if !old_locator.get_is_portable() {
|
||||
if old_locator.get_manifest_id() != new_locator.get_manifest_id() {
|
||||
info!("The app ID has changed, removing old uninstall registry entry.");
|
||||
if let Err(e) = crate::windows::registry::remove_uninstall_entry(&old_locator) {
|
||||
warn!("Failed to remove old uninstall entry ({}).", e);
|
||||
}
|
||||
}
|
||||
if let Err(e) = crate::windows::registry::write_uninstall_entry(&new_locator) {
|
||||
warn!("Failed to write new uninstall entry ({}).", e);
|
||||
}
|
||||
} else {
|
||||
info!("Skipping uninstall entry for portable app.");
|
||||
}
|
||||
|
||||
// seventh, we run the post-install hooks
|
||||
if run_hooks {
|
||||
crate::windows::run_hook(&new_app, &root_path, "--veloapp-updated", 15);
|
||||
crate::windows::run_hook(&new_locator, "--veloapp-updated", 15);
|
||||
} else {
|
||||
info!("Skipping --veloapp-updated hook.");
|
||||
}
|
||||
@@ -145,7 +161,8 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
|
||||
// to update the shortcut to point at the temp/renamed location
|
||||
let _ = remove_dir_all::remove_dir_all(&temp_path_new);
|
||||
let _ = remove_dir_all::remove_dir_all(&temp_path_old);
|
||||
crate::windows::create_or_update_manifest_lnks(root_path, &new_app, Some(old_app));
|
||||
|
||||
crate::windows::create_or_update_manifest_lnks(&new_locator, Some(old_locator));
|
||||
|
||||
// done!
|
||||
info!("Package applied successfully.");
|
||||
@@ -156,5 +173,5 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
|
||||
let _ = remove_dir_all::remove_dir_all(&temp_path_new);
|
||||
let _ = remove_dir_all::remove_dir_all(&temp_path_old);
|
||||
action?;
|
||||
Ok(new_app)
|
||||
Ok(new_locator)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
use crate::{
|
||||
dialogs,
|
||||
shared::{self, bundle, runtime_arch::RuntimeArch},
|
||||
shared::{self},
|
||||
windows,
|
||||
};
|
||||
use velopack::bundle::BundleZip;
|
||||
use velopack::locator::*;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use pretty_bytes_rust::pretty_bytes;
|
||||
use std::{
|
||||
fs::{self},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use ::windows::core::PCWSTR;
|
||||
use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use memmap2::Mmap;
|
||||
use pretty_bytes_rust::pretty_bytes;
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Result<()> {
|
||||
let osinfo = os_info::get();
|
||||
let osarch = RuntimeArch::from_current_system();
|
||||
info!("OS: {osinfo}, Arch={osarch:#?}");
|
||||
|
||||
if !windows::is_windows_7_sp1_or_greater() {
|
||||
bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
|
||||
}
|
||||
|
||||
let file = File::open(env::current_exe()?)?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
let pkg = bundle::load_bundle_from_mmap(&mmap, debug_pkg)?;
|
||||
info!("Bundle loaded successfully.");
|
||||
|
||||
pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Option<Vec<&str>>) -> Result<()> {
|
||||
// find and parse nuspec
|
||||
info!("Reading package manifest...");
|
||||
let app = pkg.read_manifest()?;
|
||||
@@ -41,7 +29,7 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
|
||||
info!(" Package Machine Architecture: {}", &app.machine_architecture);
|
||||
info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies);
|
||||
|
||||
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?;
|
||||
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app.id))?;
|
||||
|
||||
if !windows::prerequisite::prompt_and_install_all_missing(&app, None)? {
|
||||
info!("Cancelling setup. Pre-requisites not installed.");
|
||||
@@ -136,7 +124,7 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
|
||||
windows::splash::show_splash_dialog(app.title.to_owned(), splash_bytes)
|
||||
};
|
||||
|
||||
let install_result = install_impl(&pkg, &root_path, &tx);
|
||||
let install_result = install_impl(pkg, &root_path, &tx, start_args);
|
||||
let _ = tx.send(windows::splash::MSG_CLOSE);
|
||||
|
||||
if install_result.is_ok() {
|
||||
@@ -159,17 +147,19 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::mpsc::Sender<i16>) -> Result<()> {
|
||||
fn install_impl(pkg: &mut BundleZip, root_path: &PathBuf, tx: &std::sync::mpsc::Sender<i16>, start_args: Option<Vec<&str>>) -> Result<()> {
|
||||
info!("Starting installation!");
|
||||
|
||||
let app = pkg.read_manifest()?;
|
||||
let app_manifest = pkg.read_manifest()?;
|
||||
let paths = create_config_from_root_dir(root_path);
|
||||
let locator = VelopackLocator::new(paths, app_manifest);
|
||||
|
||||
// all application paths
|
||||
let updater_path = app.get_update_path(root_path);
|
||||
let packages_path = app.get_packages_path(root_path);
|
||||
let current_path = app.get_current_path(root_path);
|
||||
let nupkg_path = app.get_target_nupkg_path(root_path);
|
||||
let main_exe_path = app.get_main_exe_path(root_path);
|
||||
let updater_path = locator.get_update_path();
|
||||
let packages_path = locator.get_packages_dir();
|
||||
let current_path = locator.get_current_bin_dir();
|
||||
let nupkg_path = locator.get_ideal_local_nupkg_path(None, None);
|
||||
let main_exe_path = locator.get_main_exe_path();
|
||||
|
||||
info!("Extracting Update.exe...");
|
||||
let _ = pkg
|
||||
@@ -186,18 +176,18 @@ fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::m
|
||||
let _ = tx.send(((p as f32) / 100.0 * 80.0 + 10.0) as i16);
|
||||
})?;
|
||||
|
||||
if !Path::new(&main_exe_path).exists() {
|
||||
if !main_exe_path.exists() {
|
||||
bail!("The main executable could not be found in the package. Please contact the application author.");
|
||||
}
|
||||
|
||||
if locator.get_manifest_shortcut_locations() != ShortcutLocationFlags::NONE {
|
||||
info!("Creating shortcuts...");
|
||||
if !app.shortcut_locations.is_empty() {
|
||||
windows::create_or_update_manifest_lnks(&root_path, &app, None);
|
||||
windows::create_or_update_manifest_lnks(&locator, None);
|
||||
}
|
||||
|
||||
info!("Starting process install hook");
|
||||
if !windows::run_hook(&app, &root_path, "--veloapp-install", 30) {
|
||||
let setup_name = format!("{} Setup {}", app.title, app.version);
|
||||
if !windows::run_hook(&locator, "--veloapp-install", 30) {
|
||||
let setup_name = format!("{} Setup {}", locator.get_manifest_title(), locator.get_manifest_id());
|
||||
dialogs::show_warn(
|
||||
&setup_name,
|
||||
None,
|
||||
@@ -206,11 +196,11 @@ fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::m
|
||||
}
|
||||
|
||||
let _ = tx.send(100);
|
||||
app.write_uninstall_entry(root_path)?;
|
||||
windows::registry::write_uninstall_entry(&locator)?;
|
||||
|
||||
if !dialogs::get_silent() {
|
||||
info!("Starting app...");
|
||||
shared::start_package(&app, &root_path, None, Some("VELOPACK_FIRSTRUN"))?;
|
||||
shared::start_package(&locator, start_args, Some("VELOPACK_FIRSTRUN"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
mod apply;
|
||||
pub use apply::*;
|
||||
|
||||
mod patch;
|
||||
pub use patch::*;
|
||||
|
||||
mod start;
|
||||
pub use start::*;
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
use anyhow::{bail, Result};
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
pub fn patch(old_file: &PathBuf, patch_file: &PathBuf, output_file: &PathBuf) -> Result<()> {
|
||||
if !old_file.exists() {
|
||||
bail!("Old file does not exist: {}", old_file.to_string_lossy());
|
||||
}
|
||||
|
||||
if !patch_file.exists() {
|
||||
bail!("Patch file does not exist: {}", patch_file.to_string_lossy());
|
||||
}
|
||||
|
||||
let dict = fs::read(old_file)?;
|
||||
|
||||
info!("Loading Dictionary (Size: {})", dict.len());
|
||||
let patch = fs::OpenOptions::new().read(true).open(patch_file)?;
|
||||
let patch_reader = io::BufReader::new(patch);
|
||||
let mut decoder = zstd::Decoder::with_dictionary(patch_reader, &dict)?;
|
||||
|
||||
let window_log = fio_highbit64(dict.len() as u64) + 1;
|
||||
if window_log >= 27 {
|
||||
info!("Large File detected. Overriding windowLog to {}", window_log);
|
||||
decoder.window_log_max(window_log)?;
|
||||
}
|
||||
|
||||
info!("Decoder loaded. Beginning patch...");
|
||||
let mut output = fs::OpenOptions::new().write(true).create(true).open(output_file)?;
|
||||
io::copy(&mut decoder, &mut output)?;
|
||||
|
||||
info!("Patch applied successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fio_highbit64(v: u64) -> u32 {
|
||||
let mut count: u32 = 0;
|
||||
let mut v = v;
|
||||
v >>= 1;
|
||||
while v > 0 {
|
||||
v >>= 1;
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bundle::Manifest;
|
||||
use crate::shared::{self, OperationWait};
|
||||
use anyhow::Result;
|
||||
use velopack::locator::VelopackLocator;
|
||||
|
||||
#[allow(unused_variables, unused_imports)]
|
||||
pub fn start(
|
||||
root_dir: &PathBuf,
|
||||
app: &Manifest,
|
||||
locator: &VelopackLocator,
|
||||
wait: OperationWait,
|
||||
exe_name: Option<&String>,
|
||||
exe_args: Option<Vec<&str>>,
|
||||
@@ -24,7 +20,7 @@ pub fn start(
|
||||
shared::operation_wait(wait);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
super::start_windows_impl::start_impl(&root_dir, &app, exe_name, exe_args, legacy_args)?;
|
||||
super::start_windows_impl::start_impl(&locator, exe_name, exe_args, legacy_args)?;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
shared::start_package(&app, &root_dir, exe_args, None)?;
|
||||
|
||||
@@ -1,67 +1,71 @@
|
||||
use crate::bundle::Manifest;
|
||||
use crate::{
|
||||
dialogs,
|
||||
shared::{self, bundle, OperationWait},
|
||||
shared::{self, OperationWait},
|
||||
windows as win,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
process::Command as Process,
|
||||
};
|
||||
use velopack::bundle;
|
||||
use velopack::locator::VelopackLocator;
|
||||
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
|
||||
pub fn start_impl(
|
||||
root_dir: &PathBuf,
|
||||
app: &Manifest,
|
||||
locator: &VelopackLocator,
|
||||
exe_name: Option<&String>,
|
||||
exe_args: Option<Vec<&str>>,
|
||||
legacy_args: Option<&String>,
|
||||
) -> Result<()> {
|
||||
match shared::has_app_prefixed_folder(root_dir) {
|
||||
let root_dir = locator.get_root_dir();
|
||||
let app_title = locator.get_manifest_title();
|
||||
match shared::has_app_prefixed_folder(&root_dir) {
|
||||
Ok(has_prefix) => {
|
||||
if has_prefix {
|
||||
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)?;
|
||||
std::env::set_current_dir(&root_dir)?;
|
||||
|
||||
if let Err(e) = try_legacy_migration(root_dir, app) {
|
||||
return match try_legacy_migration(&locator) {
|
||||
Ok(new_locator) => {
|
||||
shared::start_package(&new_locator, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to migrate legacy app ({}).", e);
|
||||
dialogs::show_error(
|
||||
&app.title,
|
||||
&app_title,
|
||||
Some("Unable to start app"),
|
||||
"This app installation has been corrupted and cannot be started. Please reinstall the app.",
|
||||
);
|
||||
return Err(e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
// we can't run the normal start command, because legacy squirrel might provide an "exe name" to restart
|
||||
// which no longer exists in the package
|
||||
let (root_dir, app) = shared::detect_current_manifest()?;
|
||||
shared::start_package(&app, root_dir, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed legacy check ({}).", e),
|
||||
}
|
||||
|
||||
let current = app.get_current_path(root_dir);
|
||||
// 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 current = locator.get_current_bin_dir();
|
||||
let exe_to_execute = if let Some(exe) = exe_name {
|
||||
Path::new(¤t).join(exe)
|
||||
} else {
|
||||
let exe = app.get_main_exe_path(root_dir);
|
||||
Path::new(&exe).to_path_buf()
|
||||
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);
|
||||
}
|
||||
|
||||
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current);
|
||||
info!("About to launch: '{:?}' in dir '{:?}'", exe_to_execute, current);
|
||||
|
||||
let mut cmd = Process::new(&exe_to_execute);
|
||||
cmd.current_dir(¤t);
|
||||
@@ -77,34 +81,37 @@ pub fn start_impl(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_legacy_migration(root_dir: &PathBuf, app: &bundle::Manifest) -> Result<()> {
|
||||
let package = shared::find_latest_full_package(root_dir).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
|
||||
let bundle = bundle::load_bundle_from_file(&package.file_path)?;
|
||||
let _bundle_manifest = bundle.read_manifest()?; // this verifies it's a bundle we support
|
||||
fn try_legacy_migration(locator: &VelopackLocator) -> Result<VelopackLocator> {
|
||||
let root_dir = locator.get_root_dir();
|
||||
let current_dir = locator.get_current_bin_dir();
|
||||
let package = shared::find_latest_full_package(&locator).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
|
||||
let mut bundle = bundle::load_bundle_from_file(&package.file_path)?;
|
||||
let bundle_manifest = bundle.read_manifest()?; // this verifies it's a bundle we support
|
||||
warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate...");
|
||||
let _ = shared::force_stop_package(root_dir);
|
||||
let current_dir = app.get_current_path(root_dir);
|
||||
let _ = shared::force_stop_package(&root_dir);
|
||||
|
||||
if !Path::new(¤t_dir).exists() {
|
||||
info!("Renaming latest app-* folder to current.");
|
||||
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(root_dir)? {
|
||||
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(&root_dir)? {
|
||||
fs::rename(latest_app_dir, ¤t_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Removing old shortcuts...");
|
||||
win::remove_all_shortcuts_for_root_dir(root_dir);
|
||||
win::remove_all_shortcuts_for_root_dir(&root_dir);
|
||||
|
||||
let mut modified_app = app.clone();
|
||||
modified_app.shortcut_locations = "".to_string(); // reset, so we install new shortcuts
|
||||
// reset current manifest shortcuts, so when the new manifest is being read
|
||||
// new shortcuts will be force-created
|
||||
let modified_app = locator.clone_self_with_blank_shortcuts();
|
||||
|
||||
info!("Applying latest full package...");
|
||||
let buf = Path::new(&package.file_path).to_path_buf();
|
||||
super::apply(root_dir, &modified_app, false, OperationWait::NoWait, Some(&buf), None, false)?;
|
||||
super::apply(&modified_app, false, OperationWait::NoWait, Some(&buf), None, false)?;
|
||||
|
||||
info!("Removing old app-* folders...");
|
||||
shared::delete_app_prefixed_folders(root_dir)?;
|
||||
shared::delete_app_prefixed_folders(&root_dir)?;
|
||||
let _ = remove_dir_all::remove_dir_all(root_dir.join("staging"));
|
||||
|
||||
Ok(())
|
||||
let new_locator = locator.clone_self_with_new_manifest(&bundle_manifest);
|
||||
Ok(new_locator)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
use crate::shared::{self, bundle::Manifest};
|
||||
use crate::shared::{self};
|
||||
use velopack::{locator::VelopackLocator};
|
||||
|
||||
use crate::windows;
|
||||
use anyhow::Result;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Result<()> {
|
||||
pub fn uninstall(locator: &VelopackLocator, delete_self: bool) -> Result<()> {
|
||||
info!("Command: Uninstall");
|
||||
|
||||
fn _uninstall_impl(app: &Manifest, root_path: &PathBuf) -> bool {
|
||||
let root_path = locator.get_root_dir();
|
||||
|
||||
fn _uninstall_impl(locator: &VelopackLocator) -> bool {
|
||||
let root_path = locator.get_root_dir();
|
||||
|
||||
// the real app could be running at the moment
|
||||
let _ = shared::force_stop_package(&root_path);
|
||||
|
||||
let mut finished_with_errors = false;
|
||||
|
||||
// run uninstall hook
|
||||
windows::run_hook(&app, root_path, "--veloapp-uninstall", 60);
|
||||
windows::run_hook(&locator, "--veloapp-uninstall", 60);
|
||||
|
||||
// remove all shortcuts pointing to the app
|
||||
windows::remove_all_shortcuts_for_root_dir(&root_path);
|
||||
@@ -25,7 +30,7 @@ pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Resu
|
||||
finished_with_errors = true;
|
||||
}
|
||||
|
||||
if let Err(e) = app.remove_uninstall_entry() {
|
||||
if let Err(e) = windows::registry::remove_uninstall_entry(&locator) {
|
||||
error!("Unable to remove uninstall registry entry ({}).", e);
|
||||
// finished_with_errors = true;
|
||||
}
|
||||
@@ -35,14 +40,15 @@ pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Resu
|
||||
|
||||
// if it returns true, it was a success.
|
||||
// if it returns false, it was completed with errors which the user should be notified of.
|
||||
let result = _uninstall_impl(&app, &root_path);
|
||||
let result = _uninstall_impl(&locator);
|
||||
let app_title = locator.get_manifest_title();
|
||||
|
||||
if result {
|
||||
info!("Finished successfully.");
|
||||
shared::dialogs::show_info(format!("{} Uninstall", app.title).as_str(), None, "The application was successfully uninstalled.");
|
||||
shared::dialogs::show_info(format!("{} Uninstall", app_title).as_str(), None, "The application was successfully uninstalled.");
|
||||
} else {
|
||||
error!("Finished with errors.");
|
||||
shared::dialogs::show_uninstall_complete_with_errors_dialog(&app, None);
|
||||
shared::dialogs::show_uninstall_complete_with_errors_dialog(&app_title, None);
|
||||
}
|
||||
|
||||
let dead_path = root_path.join(".dead");
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod shared;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
pub use shared::{bundle, dialogs};
|
||||
pub use shared::{dialogs};
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
@@ -7,31 +7,9 @@ pub fn trace_logger() {
|
||||
TermLogger::init(LevelFilter::Trace, get_config(None), TerminalMode::Mixed, ColorChoice::Never).unwrap();
|
||||
}
|
||||
|
||||
pub fn default_log_location() -> PathBuf {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut my_dir = std::env::current_exe().unwrap();
|
||||
my_dir.pop();
|
||||
return my_dir.join("Velopack.log");
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return std::path::Path::new("/tmp/velopack.log").to_path_buf();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
let mut user_home = std::env::home_dir().expect("Could not locate user home directory via $HOME or /etc/passwd");
|
||||
user_home.push("Library");
|
||||
user_home.push("Logs");
|
||||
user_home.push("velopack.log");
|
||||
return user_home;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_logging(process_name: &str, file: Option<&PathBuf>, console: bool, verbose: bool, nocolor: bool) -> Result<()> {
|
||||
pub fn setup_logging(process_name: &str, file: Option<&PathBuf>, console: bool, verbose: bool) -> Result<()> {
|
||||
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
|
||||
let color_choice = if nocolor { ColorChoice::Never } else { ColorChoice::Auto };
|
||||
let color_choice = ColorChoice::Never;
|
||||
if console {
|
||||
let console_level = if verbose { LevelFilter::Debug } else { LevelFilter::Info };
|
||||
loggers.push(TermLogger::new(console_level, get_config(None), TerminalMode::Mixed, color_choice));
|
||||
|
||||
@@ -4,13 +4,59 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{arg, value_parser, Command};
|
||||
use memmap2::Mmap;
|
||||
use std::cell::RefCell;
|
||||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::rc::Rc;
|
||||
use std::{env, path::PathBuf};
|
||||
use velopack_bins::*;
|
||||
|
||||
#[used]
|
||||
#[no_mangle]
|
||||
static BUNDLE_PLACEHOLDER: [u8; 48] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package offset
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package length
|
||||
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29, // 32 bytes for bundle signature
|
||||
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4, //
|
||||
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5, //
|
||||
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d, //
|
||||
];
|
||||
|
||||
#[used]
|
||||
#[inline(never)]
|
||||
pub fn header_offset_and_length() -> (i64, i64) {
|
||||
use core::ptr;
|
||||
// Perform volatile reads to avoid optimization issues
|
||||
// TODO: refactor to use little-endian, also need to update the writer in dotnet
|
||||
unsafe {
|
||||
let offset = i64::from_ne_bytes([
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[0]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[1]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[2]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[3]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[4]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[5]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[6]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[7]),
|
||||
]);
|
||||
let length = i64::from_ne_bytes([
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[8]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[9]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[10]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[11]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[12]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[13]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[14]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[15]),
|
||||
]);
|
||||
(offset, length)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
windows::mitigate::pre_main_sideload_mitigation();
|
||||
|
||||
#[rustfmt::skip]
|
||||
@@ -20,30 +66,42 @@ fn main() -> Result<()> {
|
||||
.arg(arg!(-v --verbose "Print debug messages to console"))
|
||||
.arg(arg!(-l --log <FILE> "Enable file logging and set location").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(-t --installto <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(--nocolor "Disable colored output").hide(true));
|
||||
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..));
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
arg_config = arg_config
|
||||
.arg(arg!(-d --debug <FILE> "Debug mode, install from a nupkg file").required(false).value_parser(value_parser!(PathBuf)));
|
||||
}
|
||||
|
||||
let matches = arg_config.get_matches();
|
||||
let res = run_inner(arg_config);
|
||||
if let Err(e) = &res {
|
||||
error!("An error has occurred: {}", e);
|
||||
dialogs::show_error("Setup Error", None, format!("An error has occurred: {}", e).as_str());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_inner(arg_config: Command) -> Result<()>
|
||||
{
|
||||
let matches = arg_config.try_get_matches()?;
|
||||
|
||||
let silent = matches.get_flag("silent");
|
||||
let verbose = matches.get_flag("verbose");
|
||||
let debug = matches.get_one::<PathBuf>("debug");
|
||||
let logfile = matches.get_one::<PathBuf>("log");
|
||||
let installto = matches.get_one::<PathBuf>("installto");
|
||||
let nocolor = matches.get_flag("nocolor");
|
||||
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());
|
||||
|
||||
shared::dialogs::set_silent(silent);
|
||||
logging::setup_logging("setup", logfile, true, verbose, nocolor)?;
|
||||
dialogs::set_silent(silent);
|
||||
logging::setup_logging("setup", logfile, true, verbose)?;
|
||||
|
||||
info!("Starting Velopack Setup ({})", env!("NGBV_VERSION"));
|
||||
info!(" Location: {:?}", std::env::current_exe()?);
|
||||
info!(" Location: {:?}", env::current_exe()?);
|
||||
info!(" Silent: {}", silent);
|
||||
info!(" Verbose: {}", verbose);
|
||||
info!(" Log: {:?}", logfile);
|
||||
info!(" Install To: {:?}", installto);
|
||||
info!(" Install To: {:?}", install_to);
|
||||
if cfg!(debug_assertions) {
|
||||
info!(" Debug: {:?}", debug);
|
||||
}
|
||||
@@ -53,12 +111,40 @@ fn main() -> Result<()> {
|
||||
containing_dir.pop();
|
||||
env::set_current_dir(containing_dir)?;
|
||||
|
||||
let res = commands::install(debug, installto);
|
||||
if let Err(e) = &res {
|
||||
error!("An error has occurred: {}", e);
|
||||
dialogs::show_error("Setup Error", None, format!("An error has occurred: {}", e).as_str());
|
||||
// load the bundle which is embedded or if missing from the debug nupkg path
|
||||
let osinfo = os_info::get();
|
||||
let osarch = shared::runtime_arch::RuntimeArch::from_current_system();
|
||||
info!("OS: {osinfo}, Arch={osarch:#?}");
|
||||
|
||||
if !windows::is_windows_7_sp1_or_greater() {
|
||||
bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
|
||||
}
|
||||
|
||||
res?;
|
||||
Ok(())
|
||||
// in debug mode only, allow a nupkg to be passed in as the first argument
|
||||
if cfg!(debug_assertions) {
|
||||
if let Some(pkg) = debug {
|
||||
info!("Loading bundle from DEBUG nupkg file {:?}...", pkg);
|
||||
let mut bundle = velopack::bundle::load_bundle_from_file(pkg)?;
|
||||
commands::install(&mut bundle, install_to, exe_args)?;
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
info!("Reading bundle header...");
|
||||
let (offset, length) = header_offset_and_length();
|
||||
info!("Bundle offset = {}, length = {}", offset, length);
|
||||
|
||||
// try to load the bundle from embedded zip
|
||||
if offset > 0 && length > 0 {
|
||||
info!("Loading bundle from embedded zip...");
|
||||
let file = File::open(env::current_exe()?)?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
let zip_range: &[u8] = &mmap[offset as usize..(offset + length) as usize];
|
||||
let mut bundle = velopack::bundle::load_bundle_from_memory(&zip_range)?;
|
||||
commands::install(&mut bundle, install_to, exe_args)?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
bail!("Could not find embedded zip file. Please contact the application author.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,634 +0,0 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs::{self, File},
|
||||
io::{Cursor, Read, Seek, Write},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
use xml::reader::{EventReader, XmlEvent};
|
||||
use zip::ZipArchive;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use chrono::{Datelike, Local as DateTime};
|
||||
#[cfg(target_os = "windows")]
|
||||
use memmap2::Mmap;
|
||||
#[cfg(target_os = "windows")]
|
||||
use normpath::PathExt;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winsafe::{self as w, co, prelude::*};
|
||||
|
||||
pub trait ReadSeek: Read + Seek {}
|
||||
impl<T: Read + Seek> ReadSeek for T {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[used]
|
||||
#[no_mangle]
|
||||
static BUNDLE_PLACEHOLDER: [u8; 48] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package offset
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package length
|
||||
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29, // 32 bytes for bundle signature
|
||||
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4, //
|
||||
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5, //
|
||||
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d, //
|
||||
];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[inline(never)]
|
||||
pub fn header_offset_and_length() -> (i64, i64) {
|
||||
use core::ptr;
|
||||
// Perform volatile reads to avoid optimization issues
|
||||
// TODO: refactor to use little-endian, also need to update the writer in dotnet
|
||||
unsafe {
|
||||
let offset = i64::from_ne_bytes([
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[0]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[1]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[2]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[3]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[4]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[5]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[6]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[7]),
|
||||
]);
|
||||
let length = i64::from_ne_bytes([
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[8]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[9]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[10]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[11]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[12]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[13]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[14]),
|
||||
ptr::read_volatile(&BUNDLE_PLACEHOLDER[15]),
|
||||
]);
|
||||
(offset, length)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn load_bundle_from_mmap<'a>(mmap: &'a Mmap, debug_pkg: Option<&PathBuf>) -> Result<BundleInfo<'a>> {
|
||||
info!("Reading bundle header...");
|
||||
let (offset, length) = header_offset_and_length();
|
||||
info!("Bundle offset = {}, length = {}", offset, length);
|
||||
|
||||
let zip_range: &'a [u8] = &mmap[offset as usize..(offset + length) as usize];
|
||||
|
||||
// try to load the bundle from embedded zip
|
||||
if offset > 0 && length > 0 {
|
||||
info!("Loading bundle from embedded zip...");
|
||||
let cursor: Box<dyn ReadSeek> = Box::new(Cursor::new(zip_range));
|
||||
let zip = ZipArchive::new(cursor).map_err(|e| anyhow::Error::new(e))?;
|
||||
return Ok(BundleInfo { zip: Rc::new(RefCell::new(zip)), zip_from_file: false, zip_range: Some(zip_range), file_path: None });
|
||||
}
|
||||
|
||||
// in debug mode only, allow a nupkg to be passed in as the first argument
|
||||
if cfg!(debug_assertions) {
|
||||
if let Some(pkg) = debug_pkg {
|
||||
info!("Loading bundle from debug nupkg file...");
|
||||
return load_bundle_from_file(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Could not find embedded zip file. Please contact the application author.");
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BundleInfo<'a> {
|
||||
zip: Rc<RefCell<ZipArchive<Box<dyn ReadSeek + 'a>>>>,
|
||||
zip_from_file: bool,
|
||||
zip_range: Option<&'a [u8]>,
|
||||
file_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleInfo<'a>> {
|
||||
let file_name = file_name.as_ref();
|
||||
debug!("Loading bundle from file '{}'...", file_name.to_string_lossy());
|
||||
let file = super::retry_io(|| File::open(&file_name))?;
|
||||
let cursor: Box<dyn ReadSeek> = Box::new(file);
|
||||
let zip = ZipArchive::new(cursor)?;
|
||||
return Ok(BundleInfo { zip: Rc::new(RefCell::new(zip)), zip_from_file: true, file_path: Some(file_name.to_owned()), zip_range: None });
|
||||
}
|
||||
|
||||
impl BundleInfo<'_> {
|
||||
pub fn calculate_size(&self) -> (u64, u64) {
|
||||
let mut total_uncompressed_size = 0u64;
|
||||
let mut total_compressed_size = 0u64;
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i);
|
||||
if file.is_ok() {
|
||||
let file = file.unwrap();
|
||||
total_uncompressed_size += file.size();
|
||||
total_compressed_size += file.compressed_size();
|
||||
}
|
||||
}
|
||||
|
||||
(total_compressed_size, total_uncompressed_size)
|
||||
}
|
||||
|
||||
pub fn get_splash_bytes(&self) -> Option<Vec<u8>> {
|
||||
let splash_idx = self.find_zip_file(|name| name.contains("splashimage"));
|
||||
if splash_idx.is_none() {
|
||||
warn!("Could not find splash image in bundle.");
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
let sf = archive.by_index(splash_idx.unwrap());
|
||||
if sf.is_err() {
|
||||
warn!("Could not find splash image in bundle.");
|
||||
return None;
|
||||
}
|
||||
|
||||
let res: Result<Vec<u8>, _> = sf.unwrap().bytes().collect();
|
||||
if res.is_err() {
|
||||
warn!("Could not find splash image in bundle.");
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = res.unwrap();
|
||||
if bytes.is_empty() {
|
||||
warn!("Could not find splash image in bundle.");
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
pub fn find_zip_file<F>(&self, predicate: F) -> Option<usize>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
for i in 0..archive.len() {
|
||||
if let Ok(file) = archive.by_index(i) {
|
||||
let name = file.name();
|
||||
if predicate(name) {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn extract_zip_idx_to_path<T: AsRef<Path>>(&self, index: usize, path: T) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
debug!("Extracting zip file to path: {}", path.to_string_lossy());
|
||||
let p = PathBuf::from(path);
|
||||
let parent = p.parent().unwrap();
|
||||
|
||||
if !parent.exists() {
|
||||
debug!("Creating parent directory: {:?}", parent);
|
||||
super::retry_io(|| fs::create_dir_all(parent))?;
|
||||
}
|
||||
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
let mut file = archive.by_index(index)?;
|
||||
let mut outfile = super::retry_io(|| File::create(path))?;
|
||||
let mut buffer = [0; 64000]; // Use a 64KB buffer; good balance for large/small files.
|
||||
|
||||
debug!("Writing normal file to disk with 64k buffer: {:?}", path);
|
||||
loop {
|
||||
let len = file.read(&mut buffer)?;
|
||||
if len == 0 {
|
||||
break; // End of file
|
||||
}
|
||||
outfile.write_all(&buffer[..len])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_zip_predicate_to_path<F, T: AsRef<Path>>(&self, predicate: F, path: T) -> Result<usize>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
let idx = self.find_zip_file(predicate);
|
||||
if idx.is_none() {
|
||||
bail!("Could not find file in bundle.");
|
||||
}
|
||||
let idx = idx.unwrap();
|
||||
self.extract_zip_idx_to_path(idx, path)?;
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn create_symlink(link_path: &PathBuf, target_path: &PathBuf) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
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(),
|
||||
absolute_path.is_file(),
|
||||
absolute_path.is_dir(),
|
||||
target_path.to_string_lossy()
|
||||
);
|
||||
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 {
|
||||
bail!("Could not create symlink: target is not a file or directory.")
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
std::os::unix::fs::symlink(target_path, link_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn extract_lib_contents_to_path<P: AsRef<Path>, F: Fn(i16)>(&self, current_path: P, progress: F) -> Result<()> {
|
||||
let current_path = current_path.as_ref();
|
||||
let files = self.get_file_names()?;
|
||||
let num_files = files.len();
|
||||
|
||||
info!("Extracting {} app files to '{}'...", num_files, current_path.to_string_lossy());
|
||||
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
|
||||
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
|
||||
let symlink_regex = Regex::new(".__symlink$").unwrap();
|
||||
let updater_idx = self.find_zip_file(|name| name.ends_with("Squirrel.exe"));
|
||||
|
||||
// for legacy support, we still extract the nuspec file to the current dir.
|
||||
// in newer versions, the nuspec is in the current dir in the package itself.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let nuspec_path = current_path.join("sq.version");
|
||||
let _ = self
|
||||
.extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path)
|
||||
.map_err(|_| anyhow!("This package is missing a nuspec manifest."))?;
|
||||
}
|
||||
|
||||
// we extract the symlinks after, because the target must exist.
|
||||
let mut symlinks: Vec<(usize, PathBuf)> = Vec::new();
|
||||
|
||||
for (i, key) in files.iter().enumerate() {
|
||||
if Some(i) == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") {
|
||||
debug!(" {} Skipped '{}'", i, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path_in_zip = re.replace(key, "").to_string();
|
||||
let file_path_on_disk = Path::new(¤t_path).join(&file_path_in_zip);
|
||||
|
||||
if symlink_regex.is_match(&file_path_in_zip) {
|
||||
let sym_key = symlink_regex.replace(&file_path_in_zip, "").to_string();
|
||||
let file_path_on_disk = Path::new(¤t_path).join(&sym_key);
|
||||
symlinks.push((i, file_path_on_disk));
|
||||
continue;
|
||||
}
|
||||
|
||||
if stub_regex.is_match(&file_path_in_zip) {
|
||||
// let stub_key = stub_regex.replace(&file_path_in_zip, ".exe").to_string();
|
||||
// file_path_on_disk = root_path.join(&stub_key);
|
||||
debug!(" {} Skipped Stub (obsolete) '{}'", i, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// on windows, the zip paths are / and should be \ instead
|
||||
#[cfg(target_os = "windows")]
|
||||
let file_path_on_disk = file_path_on_disk.normalize_virtually()?;
|
||||
#[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());
|
||||
self.extract_zip_idx_to_path(i, &file_path_on_disk)?;
|
||||
|
||||
// on macos, we need to chmod +x the executable files
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(true) = super::macho::is_macho_image(&file_path_on_disk) {
|
||||
if let Err(e) = std::fs::set_permissions(&file_path_on_disk, std::fs::Permissions::from_mode(0o755)) {
|
||||
warn!("Failed to set executable permissions on '{}': {}", file_path_on_disk.to_string_lossy(), e);
|
||||
} else {
|
||||
info!(" {} Set executable permissions on '{}'", i, file_path_on_disk.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress(((i as f32 / num_files as f32) * 100.0) as i16);
|
||||
}
|
||||
|
||||
// we extract the symlinks after, because the target must exist.
|
||||
for (i, link_path) in symlinks {
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
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);
|
||||
|
||||
let contents = contents.trim_end_matches('/');
|
||||
#[cfg(target_os = "windows")]
|
||||
let contents = contents.replace("/", "\\");
|
||||
let contents = PathBuf::from(contents);
|
||||
|
||||
let parent = link_path.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
debug!("Creating parent directory: {:?}", parent);
|
||||
super::retry_io(|| fs::create_dir_all(parent))?;
|
||||
}
|
||||
super::retry_io(|| Self::create_symlink(&link_path, &contents))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_manifest(&self) -> Result<Manifest> {
|
||||
let nuspec_idx = self
|
||||
.find_zip_file(|name| name.ends_with(".nuspec"))
|
||||
.ok_or_else(|| anyhow!("This installer is missing a package manifest (.nuspec). Please contact the application author."))?;
|
||||
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)?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub fn copy_bundle_to_file<T: AsRef<str>>(&self, nupkg_path: T) -> Result<()> {
|
||||
let nupkg_path = nupkg_path.as_ref();
|
||||
if self.zip_from_file {
|
||||
super::retry_io(|| fs::copy(self.file_path.clone().unwrap(), nupkg_path))?;
|
||||
} else {
|
||||
super::retry_io(|| fs::write(nupkg_path, self.zip_range.unwrap()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let archive = self.zip.borrow();
|
||||
archive.len()
|
||||
}
|
||||
|
||||
pub fn get_file_names(&self) -> Result<Vec<String>> {
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i)?;
|
||||
let key = file.enclosed_name().ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Could not extract file safely ({}). Ensure no paths in archive are absolute or point to a path outside the archive.",
|
||||
file.name()
|
||||
)
|
||||
})?;
|
||||
files.push(key.to_string_lossy().to_string());
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, derivative::Derivative, Clone)]
|
||||
#[derivative(Default)]
|
||||
pub struct Manifest {
|
||||
pub id: String,
|
||||
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
|
||||
pub version: Version,
|
||||
pub title: String,
|
||||
pub authors: String,
|
||||
pub description: String,
|
||||
pub machine_architecture: String,
|
||||
pub runtime_dependencies: String,
|
||||
pub main_exe: String,
|
||||
pub os: String,
|
||||
pub os_min_version: String,
|
||||
pub channel: String,
|
||||
pub shortcut_locations: String,
|
||||
pub shortcut_amuid: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl Manifest {
|
||||
const UNINST_STR: &'static str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
|
||||
|
||||
pub fn get_update_path(&self, root_path: &PathBuf) -> String {
|
||||
root_path.join("Update.exe").to_string_lossy().to_string()
|
||||
}
|
||||
pub fn get_main_exe_path(&self, root_path: &PathBuf) -> String {
|
||||
root_path.join("current").join(&self.main_exe).to_string_lossy().to_string()
|
||||
}
|
||||
pub fn get_packages_path(&self, root_path: &PathBuf) -> String {
|
||||
root_path.join("packages").to_string_lossy().to_string()
|
||||
}
|
||||
pub fn get_current_path(&self, root_path: &PathBuf) -> String {
|
||||
root_path.join("current").to_string_lossy().to_string()
|
||||
}
|
||||
pub fn get_nuspec_path(&self, root_path: &PathBuf) -> String {
|
||||
root_path.join("current").join("sq.version").to_string_lossy().to_string()
|
||||
}
|
||||
pub fn get_target_nupkg_path(&self, root_path: &PathBuf) -> String {
|
||||
root_path.join("packages").join(format!("{}-{}-full.nupkg", self.id, self.version)).to_string_lossy().to_string()
|
||||
}
|
||||
pub fn write_uninstall_entry(&self, root_path: &PathBuf) -> Result<()> {
|
||||
info!("Writing uninstall registry key...");
|
||||
let root_path_str = root_path.to_string_lossy().to_string();
|
||||
let main_exe_path = self.get_main_exe_path(root_path);
|
||||
let updater_path = self.get_update_path(root_path);
|
||||
|
||||
let folder_size = fs_extra::dir::get_size(&root_path).unwrap();
|
||||
let sver = &self.version;
|
||||
let sver_str = format!("{}.{}.{}", sver.major, sver.minor, sver.patch);
|
||||
|
||||
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 = format!("\"{}\" --uninstall --silent", updater_path);
|
||||
|
||||
let reg_uninstall =
|
||||
w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
|
||||
let reg_app = reg_uninstall.RegCreateKeyEx(&self.id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
|
||||
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
|
||||
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(self.title.to_owned()))?;
|
||||
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(sver_str))?;
|
||||
reg_app.RegSetKeyValue(None, Some("InstallDate"), w::RegistryValue::Sz(formatted_date))?;
|
||||
reg_app.RegSetKeyValue(None, Some("InstallLocation"), w::RegistryValue::Sz(root_path_str.to_owned()))?;
|
||||
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(self.authors.to_owned()))?;
|
||||
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?;
|
||||
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?;
|
||||
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?;
|
||||
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?;
|
||||
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?;
|
||||
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn remove_uninstall_entry(&self) -> Result<()> {
|
||||
info!("Removing uninstall registry keys...");
|
||||
let reg_uninstall =
|
||||
w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
|
||||
reg_uninstall.RegDeleteKey(&self.id)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
|
||||
let mut obj: Manifest = Default::default();
|
||||
let cursor = Cursor::new(xml);
|
||||
let parser = EventReader::new(cursor);
|
||||
let mut vec: Vec<String> = Vec::new();
|
||||
for e in parser {
|
||||
match e {
|
||||
Ok(XmlEvent::StartElement { name, .. }) => {
|
||||
vec.push(name.local_name);
|
||||
}
|
||||
Ok(XmlEvent::Characters(text)) => {
|
||||
if vec.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let el_name = vec.last().unwrap();
|
||||
if el_name == "id" {
|
||||
obj.id = text;
|
||||
} else if el_name == "version" {
|
||||
obj.version = Version::parse(&text)?;
|
||||
} else if el_name == "title" {
|
||||
obj.title = text;
|
||||
} else if el_name == "authors" {
|
||||
obj.authors = text;
|
||||
} else if el_name == "description" {
|
||||
obj.description = text;
|
||||
} else if el_name == "machineArchitecture" {
|
||||
obj.machine_architecture = text;
|
||||
} else if el_name == "runtimeDependencies" {
|
||||
obj.runtime_dependencies = text;
|
||||
} else if el_name == "mainExe" {
|
||||
obj.main_exe = text;
|
||||
} else if el_name == "os" {
|
||||
obj.os = text;
|
||||
} else if el_name == "osMinVersion" {
|
||||
obj.os_min_version = text;
|
||||
} else if el_name == "channel" {
|
||||
obj.channel = text;
|
||||
} else if el_name == "shortcutLocations" {
|
||||
obj.shortcut_locations = text;
|
||||
} else if el_name == "shortcutAmuid" {
|
||||
obj.shortcut_amuid = text;
|
||||
}
|
||||
}
|
||||
Ok(XmlEvent::EndElement { .. }) => {
|
||||
vec.pop();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error: {e}");
|
||||
break;
|
||||
}
|
||||
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.id.is_empty() {
|
||||
bail!("Missing 'id' in package manifest. Please contact the application author.");
|
||||
}
|
||||
|
||||
if obj.version == Version::new(0, 0, 0) {
|
||||
bail!("Missing 'version' in package manifest. Please contact the application author.");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if obj.main_exe.is_empty() {
|
||||
bail!("Missing 'mainExe' in package manifest. Please contact the application author.");
|
||||
}
|
||||
|
||||
if obj.title.is_empty() {
|
||||
obj.title = obj.id.clone();
|
||||
}
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derivative::Derivative)]
|
||||
#[derivative(Default)]
|
||||
pub struct EntryNameInfo {
|
||||
pub name: String,
|
||||
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
|
||||
pub version: Version,
|
||||
pub is_delta: bool,
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
impl EntryNameInfo {
|
||||
pub fn load_manifest(&self) -> Result<Manifest> {
|
||||
let path = Path::new(&self.file_path).to_path_buf();
|
||||
let bundle = load_bundle_from_file(&path)?;
|
||||
bundle.read_manifest()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ENTRY_SUFFIX_FULL: Regex = Regex::new(r"(?i)-full.nupkg$").unwrap();
|
||||
static ref ENTRY_SUFFIX_DELTA: Regex = Regex::new(r"(?i)-delta.nupkg$").unwrap();
|
||||
static ref ENTRY_VERSION_START: Regex = Regex::new(r"[\.-](0|[1-9]\d*)\.(0|[1-9]\d*)($|[^\d])").unwrap();
|
||||
}
|
||||
|
||||
pub fn parse_package_file_path(path: PathBuf) -> Option<EntryNameInfo> {
|
||||
let name = path.file_name()?.to_string_lossy().to_string();
|
||||
let m = parse_package_file_name(name);
|
||||
if m.is_some() {
|
||||
let mut m = m.unwrap();
|
||||
m.file_path = path.to_string_lossy().to_string();
|
||||
return Some(m);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn parse_package_file_name<T: AsRef<str>>(name: T) -> Option<EntryNameInfo> {
|
||||
let name = name.as_ref();
|
||||
let full = ENTRY_SUFFIX_FULL.is_match(name);
|
||||
let delta = ENTRY_SUFFIX_DELTA.is_match(name);
|
||||
if !full && !delta {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 ver_idx = ENTRY_VERSION_START.find(&name_and_ver);
|
||||
if ver_idx.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ver_idx = ver_idx.unwrap().start();
|
||||
entry.name = name_and_ver[0..ver_idx].to_string();
|
||||
let ver_idx = ver_idx + 1;
|
||||
let version = name_and_ver[ver_idx..].to_string();
|
||||
|
||||
let sv = Version::parse(&version);
|
||||
if sv.is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
entry.version = sv.unwrap();
|
||||
return Some(entry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_package_file_name() {
|
||||
// test no rid
|
||||
let entry = parse_package_file_name("Velopack-1.0.0-full.nupkg").unwrap();
|
||||
assert_eq!(entry.name, "Velopack");
|
||||
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
|
||||
assert_eq!(entry.is_delta, false);
|
||||
|
||||
let entry = parse_package_file_name("Velopack-1.0.0-delta.nupkg").unwrap();
|
||||
assert_eq!(entry.name, "Velopack");
|
||||
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
|
||||
assert_eq!(entry.is_delta, true);
|
||||
|
||||
let entry = parse_package_file_name("My.Cool-App-1.1.0-full.nupkg").unwrap();
|
||||
assert_eq!(entry.name, "My.Cool-App");
|
||||
assert_eq!(entry.version, Version::parse("1.1.0").unwrap());
|
||||
assert_eq!(entry.is_delta, false);
|
||||
|
||||
// test invalid names
|
||||
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.nupkg.zip").is_none());
|
||||
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());
|
||||
}
|
||||
@@ -43,14 +43,14 @@ pub fn show_ok_cancel(title: &str, header: Option<&str>, body: &str, ok_text: Op
|
||||
generate_confirm(title, header, body, ok_text, btns, DialogIcon::Warning).map(|dlg_id| dlg_id == DialogResult::Ok).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn ask_user_to_elevate(app_to: &crate::bundle::Manifest) -> Result<()> {
|
||||
pub fn ask_user_to_elevate(app_title: &str, new_version: &str) -> Result<()> {
|
||||
if get_silent() {
|
||||
bail!("Not allowed to ask for elevated permissions because --silent flag is set.");
|
||||
}
|
||||
|
||||
let title = format!("{} Update", app_to.title);
|
||||
let title = format!("{} Update", app_title);
|
||||
let body =
|
||||
format!("{} would like to update to version {}, but requires elevated permissions to do so. Would you like to proceed?", app_to.title, app_to.version);
|
||||
format!("{} would like to update to version {}, but requires elevated permissions to do so. Would you like to proceed?", app_title, new_version);
|
||||
|
||||
info!("Showing user elevation prompt?");
|
||||
if show_ok_cancel(title.as_str(), None, body.as_str(), Some("Install Update")) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::{bundle::Manifest, dialogs_common::*, dialogs_const::*};
|
||||
use super::{dialogs_common::*, dialogs_const::*};
|
||||
use velopack::bundle::Manifest;
|
||||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
use winsafe::{self as w, co, prelude::*, WString};
|
||||
use velopack::locator::{auto_locate_app_manifest, LocationContext};
|
||||
|
||||
pub fn show_restart_required(app: &Manifest) {
|
||||
show_warn(
|
||||
@@ -51,13 +53,13 @@ pub fn show_setup_missing_dependencies_dialog(app: &Manifest, depedency_string:
|
||||
)
|
||||
}
|
||||
|
||||
pub fn show_uninstall_complete_with_errors_dialog(app: &Manifest, log_path: Option<&PathBuf>) {
|
||||
pub fn show_uninstall_complete_with_errors_dialog(app_title: &str, log_path: Option<&PathBuf>) {
|
||||
if get_silent() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut setup_name = WString::from_str(format!("{} Uninstall", app.title));
|
||||
let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app.title));
|
||||
let mut setup_name = WString::from_str(format!("{} Uninstall", app_title));
|
||||
let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app_title));
|
||||
let mut content = WString::from_str(
|
||||
"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.",
|
||||
);
|
||||
@@ -84,7 +86,7 @@ pub fn show_uninstall_complete_with_errors_dialog(app: &Manifest, log_path: Opti
|
||||
let _ = w::TaskDialogIndirect(&config, None);
|
||||
}
|
||||
|
||||
pub fn show_processes_locking_folder_dialog(app: &Manifest, process_names: &str) -> DialogResult {
|
||||
pub fn show_processes_locking_folder_dialog(app_title: &str, app_version: &str, process_names: &str) -> DialogResult {
|
||||
if get_silent() {
|
||||
return DialogResult::Cancel;
|
||||
}
|
||||
@@ -92,13 +94,13 @@ pub fn show_processes_locking_folder_dialog(app: &Manifest, process_names: &str)
|
||||
let mut config: w::TASKDIALOGCONFIG = Default::default();
|
||||
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
|
||||
|
||||
let mut update_name = WString::from_str(format!("{} Update {}", app.title, app.version));
|
||||
let mut instruction = WString::from_str(format!("{} Update", app.title));
|
||||
let mut update_name = WString::from_str(format!("{} Update {}", app_title, app_version));
|
||||
let mut instruction = WString::from_str(format!("{} Update", app_title));
|
||||
|
||||
let mut content = WString::from_str(format!(
|
||||
"There are programs ({}) preventing the {} update from proceeding. \n\n\
|
||||
You can press Continue to have this updater attempt to close them automatically, or if you've closed them yourself press Retry for the updater to check again.",
|
||||
process_names, app.title));
|
||||
process_names, app_title));
|
||||
|
||||
let mut btn_retry_txt = WString::from_str("Retry\nTry again if you've closed the program(s)");
|
||||
let mut btn_continue_txt = WString::from_str("Continue\nAttempt to close the program(s) automatically");
|
||||
@@ -145,19 +147,19 @@ pub fn show_overwrite_repair_dialog(app: &Manifest, root_path: &PathBuf, root_is
|
||||
let mut btn_cancel_txt = WString::from_str("Cancel\nBackup or save your work first");
|
||||
|
||||
// if we can detect the current app version, we call it "Update" or "Downgrade"
|
||||
let possible_update = root_path.join("Update.exe");
|
||||
let old_app = super::detect_manifest_from_update_path(&possible_update).map(|v| v.1).ok();
|
||||
if let Some(old) = old_app {
|
||||
if old.version < app.version {
|
||||
let old_app = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(root_path.to_owned()));
|
||||
if let Ok(old) = old_app {
|
||||
let old_version = old.get_manifest_version();
|
||||
if old_version < app.version {
|
||||
instruction = WString::from_str(format!("An older version of {} is installed.", app.title));
|
||||
content = WString::from_str(format!("Would you like to update from {} to {}?", old.version, app.version));
|
||||
content = WString::from_str(format!("Would you like to update from {} to {}?", old_version, app.version));
|
||||
btn_yes_txt = WString::from_str(format!("Update\nTo version {}", app.version));
|
||||
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
|
||||
} else if old.version > app.version {
|
||||
} else if old_version > app.version {
|
||||
instruction = WString::from_str(format!("A newer version of {} is installed.", app.title));
|
||||
content = WString::from_str(format!(
|
||||
"You already have {} installed. Would you like to downgrade this application to an older version?",
|
||||
old.version
|
||||
old_version
|
||||
));
|
||||
btn_yes_txt = WString::from_str(format!("Downgrade\nTo version {}", app.version));
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
use crate::shared;
|
||||
use anyhow::Result;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> Result<()>
|
||||
where
|
||||
A: FnMut(i16),
|
||||
{
|
||||
let agent = get_download_agent()?;
|
||||
let response = agent.get(url).call()?;
|
||||
|
||||
let total_size = response.header("Content-Length").and_then(|s| s.parse::<u64>().ok());
|
||||
let mut file = shared::retry_io(|| File::create(file_path))?;
|
||||
|
||||
const CHUNK_SIZE: usize = 2 * 1024 * 1024; // 2MB
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut buffer = vec![0; CHUNK_SIZE];
|
||||
let mut reader = response.into_reader();
|
||||
|
||||
let mut last_progress = 0;
|
||||
|
||||
while let Ok(size) = reader.read(&mut buffer) {
|
||||
if size == 0 {
|
||||
break; // End of stream
|
||||
}
|
||||
file.write_all(&buffer[..size])?;
|
||||
downloaded += size as u64;
|
||||
|
||||
if total_size.is_some() {
|
||||
// floor to nearest 5% to reduce message spam
|
||||
let new_progress = (downloaded as f64 / total_size.unwrap() as f64 * 20.0).floor() as i16 * 5;
|
||||
if new_progress > last_progress {
|
||||
last_progress = new_progress;
|
||||
progress(last_progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn download_url_as_string(url: &str) -> Result<String> {
|
||||
let agent = get_download_agent()?;
|
||||
let r = agent.get(url).call()?.into_string()?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn get_download_agent() -> Result<ureq::Agent> {
|
||||
#[allow(unused_mut)]
|
||||
let mut tls_builder = native_tls::TlsConnector::builder();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if !crate::windows::is_windows_10_or_greater() {
|
||||
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
|
||||
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
|
||||
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
|
||||
tls_builder.danger_accept_invalid_certs(true);
|
||||
tls_builder.danger_accept_invalid_hostnames(true);
|
||||
}
|
||||
|
||||
let tls_connector = tls_builder.build()?;
|
||||
Ok(ureq::AgentBuilder::new().tls_connector(tls_connector.into()).build())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_uses_tls_and_encoding_correctly() {
|
||||
assert_eq!(
|
||||
download_url_as_string("https://dotnetcli.blob.core.windows.net/dotnet/WindowsDesktop/5.0/latest.version").unwrap(),
|
||||
"5.0.17"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_file_reports_progress() {
|
||||
// https://www.ip-toolbox.com/speedtest-files/
|
||||
let test_file = "https://proof.ovh.net/files/10Mb.dat";
|
||||
let mut prog_count = 0;
|
||||
let mut last_prog = 0;
|
||||
|
||||
download_url_to_file(test_file, "test_download_file_reports_progress.txt", |p| {
|
||||
assert!(p >= last_prog);
|
||||
prog_count += 1;
|
||||
last_prog = p;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(prog_count >= 4);
|
||||
assert!(prog_count <= 20);
|
||||
assert_eq!(last_prog, 100);
|
||||
|
||||
let p = std::path::Path::new("test_download_file_reports_progress.txt");
|
||||
let meta = p.metadata().unwrap();
|
||||
let len = meta.len();
|
||||
|
||||
assert_eq!(len, 10 * 1024 * 1024);
|
||||
std::fs::remove_file(p).unwrap();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod bundle;
|
||||
pub mod download;
|
||||
// pub mod bundle;
|
||||
// pub mod download;
|
||||
pub mod macho;
|
||||
pub mod runtime_arch;
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ use windows::Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInf
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
|
||||
use winsafe::{self as w, co, prelude::*};
|
||||
|
||||
use super::bundle::{self, EntryNameInfo, Manifest};
|
||||
use velopack::bundle::{self, EntryNameInfo};
|
||||
use velopack::locator::VelopackLocator;
|
||||
|
||||
pub fn wait_for_pid_to_exit(pid: u32, ms_to_wait: u32) -> Result<()> {
|
||||
info!("Waiting {}ms for process ({}) to exit.", ms_to_wait, pid);
|
||||
@@ -169,12 +170,10 @@ fn _force_stop_package<P: AsRef<Path>>(root_dir: P) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_package<P: AsRef<Path>>(app: &Manifest, root_dir: P, exe_args: Option<Vec<&str>>, set_env: Option<&str>) -> Result<()> {
|
||||
let root_dir = root_dir.as_ref().to_path_buf();
|
||||
let current = app.get_current_path(&root_dir);
|
||||
let exe = app.get_main_exe_path(&root_dir);
|
||||
pub fn start_package(locator: &VelopackLocator, exe_args: Option<Vec<&str>>, set_env: Option<&str>) -> Result<()> {
|
||||
let current = locator.get_current_bin_dir();
|
||||
let exe_to_execute = locator.get_main_exe_path();
|
||||
|
||||
let exe_to_execute = std::path::Path::new(&exe);
|
||||
if !exe_to_execute.exists() {
|
||||
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy());
|
||||
}
|
||||
@@ -189,7 +188,7 @@ pub fn start_package<P: AsRef<Path>>(app: &Manifest, root_dir: P, exe_args: Opti
|
||||
psi.env(env, "true");
|
||||
}
|
||||
|
||||
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current);
|
||||
info!("About to launch: '{:?}' in dir '{:?}'", exe_to_execute, current);
|
||||
info!("Args: {:?}", psi.get_args());
|
||||
let child = psi.spawn().map_err(|z| anyhow!("Failed to start application ({}).", z))?;
|
||||
let _ = unsafe { AllowSetForegroundWindow(child.id()) };
|
||||
@@ -197,20 +196,6 @@ pub fn start_package<P: AsRef<Path>>(app: &Manifest, root_dir: P, exe_args: Opti
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn detect_manifest_from_update_path(update_exe: &PathBuf) -> Result<(PathBuf, Manifest)> {
|
||||
let root_path = update_exe.parent().unwrap().to_path_buf();
|
||||
let app = find_manifest_from_root_dir(&root_path)
|
||||
.map_err(|m| anyhow!("Unable to read application manifest ({}). Is this a properly installed application?", m))?;
|
||||
info!("Loaded manifest for application: {}", app.id);
|
||||
info!("Root Directory: {}", root_path.to_string_lossy());
|
||||
Ok((root_path, app))
|
||||
}
|
||||
|
||||
pub fn detect_current_manifest() -> Result<(PathBuf, Manifest)> {
|
||||
let me = std::env::current_exe()?;
|
||||
detect_manifest_from_update_path(&me)
|
||||
}
|
||||
|
||||
pub fn get_app_prefixed_folders<P: AsRef<Path>>(parent_path: P) -> Result<Vec<PathBuf>> {
|
||||
let parent_path = parent_path.as_ref();
|
||||
let re = Regex::new(r"(?i)^app-")?;
|
||||
@@ -277,37 +262,37 @@ fn parse_version_from_folder_name(folder_name: &str) -> Option<Version> {
|
||||
folder_name.strip_prefix("app-").and_then(|v| Version::parse(v).ok())
|
||||
}
|
||||
|
||||
fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> {
|
||||
// default to checking current/sq.version
|
||||
let cm = find_current_manifest(root_path);
|
||||
if cm.is_ok() {
|
||||
return cm;
|
||||
}
|
||||
// fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> {
|
||||
// // default to checking current/sq.version
|
||||
// let cm = find_current_manifest(root_path);
|
||||
// if cm.is_ok() {
|
||||
// return cm;
|
||||
// }
|
||||
//
|
||||
// // if that fails, check for latest full package
|
||||
// warn!("Unable to find current manifest, checking for latest full package. (LEGACY MODE)");
|
||||
// let latest = find_latest_full_package(root_path);
|
||||
// if let Some(latest) = latest {
|
||||
// let mani = latest.load_manifest()?;
|
||||
// return Ok(mani);
|
||||
// }
|
||||
//
|
||||
// bail!("Unable to locate manifest or package.");
|
||||
// }
|
||||
//
|
||||
// fn find_current_manifest(root_path: &PathBuf) -> Result<Manifest> {
|
||||
// let m = Manifest::default();
|
||||
// let nuspec_path = m.get_nuspec_path(root_path);
|
||||
// if Path::new(&nuspec_path).exists() {
|
||||
// if let Ok(nuspec) = super::retry_io(|| std::fs::read_to_string(&nuspec_path)) {
|
||||
// return Ok(bundle::read_manifest_from_string(&nuspec)?);
|
||||
// }
|
||||
// }
|
||||
// bail!("Unable to read nuspec file in current directory.")
|
||||
// }
|
||||
|
||||
// if that fails, check for latest full package
|
||||
warn!("Unable to find current manifest, checking for latest full package. (LEGACY MODE)");
|
||||
let latest = find_latest_full_package(root_path);
|
||||
if let Some(latest) = latest {
|
||||
let mani = latest.load_manifest()?;
|
||||
return Ok(mani);
|
||||
}
|
||||
|
||||
bail!("Unable to locate manifest or package.");
|
||||
}
|
||||
|
||||
fn find_current_manifest(root_path: &PathBuf) -> Result<Manifest> {
|
||||
let m = Manifest::default();
|
||||
let nuspec_path = m.get_nuspec_path(root_path);
|
||||
if Path::new(&nuspec_path).exists() {
|
||||
if let Ok(nuspec) = super::retry_io(|| std::fs::read_to_string(&nuspec_path)) {
|
||||
return Ok(bundle::read_manifest_from_string(&nuspec)?);
|
||||
}
|
||||
}
|
||||
bail!("Unable to read nuspec file in current directory.")
|
||||
}
|
||||
|
||||
pub fn find_latest_full_package(root_path: &PathBuf) -> Option<EntryNameInfo> {
|
||||
let packages = get_all_packages(root_path);
|
||||
pub fn find_latest_full_package(locator: &VelopackLocator) -> Option<EntryNameInfo> {
|
||||
let packages = get_all_packages(locator);
|
||||
let mut latest: Option<EntryNameInfo> = None;
|
||||
for pkg in packages {
|
||||
if pkg.is_delta {
|
||||
@@ -325,15 +310,14 @@ pub fn find_latest_full_package(root_path: &PathBuf) -> Option<EntryNameInfo> {
|
||||
latest
|
||||
}
|
||||
|
||||
fn get_all_packages(root_path: &PathBuf) -> Vec<EntryNameInfo> {
|
||||
let m = Manifest::default();
|
||||
let packages = m.get_packages_path(root_path);
|
||||
fn get_all_packages(locator: &VelopackLocator) -> Vec<EntryNameInfo> {
|
||||
let packages = locator.get_packages_dir();
|
||||
let mut vec = Vec::new();
|
||||
debug!("Scanning for packages in {:?}", packages);
|
||||
if let Ok(entries) = std::fs::read_dir(packages) {
|
||||
if let Ok(entries) = fs::read_dir(packages) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
if let Some(pkg) = super::bundle::parse_package_file_path(entry.path()) {
|
||||
if let Some(pkg) = bundle::parse_package_file_path(entry.path()) {
|
||||
debug!("Found package: {}", entry.path().to_string_lossy());
|
||||
vec.push(pkg);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ extern crate log;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::{arg, value_parser, ArgMatches, Command};
|
||||
use std::{env, path::PathBuf};
|
||||
use velopack::locator;
|
||||
use velopack::locator::{auto_locate_app_manifest, LocationContext};
|
||||
use velopack_bins::*;
|
||||
|
||||
#[rustfmt::skip]
|
||||
@@ -20,7 +22,7 @@ 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 preceeded 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..))
|
||||
)
|
||||
.subcommand(Command::new("start")
|
||||
.about("Starts the currently installed version of the application")
|
||||
@@ -28,7 +30,7 @@ fn root_command() -> Command {
|
||||
.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 preceeded 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..))
|
||||
.long_flag_aliases(vec!["processStart", "processStartAndWait"])
|
||||
)
|
||||
.subcommand(Command::new("patch")
|
||||
@@ -41,11 +43,13 @@ fn root_command() -> Command {
|
||||
.about("Prints the current version of the application")
|
||||
)
|
||||
.arg(arg!(--verbose "Print debug messages to console / log").global(true))
|
||||
.arg(arg!(--nocolor "Disable colored output").hide(true).global(true))
|
||||
.arg(arg!(-s --silent "Don't show any prompts / dialogs").global(true))
|
||||
.arg(arg!(-l --log <PATH> "Override the default log file location").global(true).value_parser(value_parser!(PathBuf)))
|
||||
// Legacy arguments should not be fully removed if it's possible to keep them
|
||||
// Reason being is clap.ignore_errors(true) is not 100%, and sometimes old args can trip things up.
|
||||
.arg(arg!(--forceLatest "Legacy argument").hide(true).global(true))
|
||||
.arg(arg!(-r --restart "Legacy argument").hide(true).global(true))
|
||||
.arg(arg!(--nocolor "Legacy argument").hide(true).global(true))
|
||||
.ignore_errors(true)
|
||||
.disable_help_subcommand(true)
|
||||
.flatten_help(true);
|
||||
@@ -122,16 +126,11 @@ fn main() -> Result<()> {
|
||||
|
||||
let verbose = get_flag_or_false(&matches, "verbose");
|
||||
let silent = get_flag_or_false(&matches, "silent");
|
||||
let nocolor = get_flag_or_false(&matches, "nocolor");
|
||||
let log_file = matches.get_one("log");
|
||||
|
||||
dialogs::set_silent(silent);
|
||||
if let Some(log_file) = log_file {
|
||||
logging::setup_logging("update", Some(&log_file), true, verbose, nocolor)?;
|
||||
} else {
|
||||
let default_log_file = logging::default_log_location();
|
||||
logging::setup_logging("update", Some(&default_log_file), true, verbose, nocolor)?;
|
||||
}
|
||||
let desired_log_file = log_file.cloned().unwrap_or(locator::default_log_location(LocationContext::IAmUpdateExe));
|
||||
logging::setup_logging("update", Some(&desired_log_file), true, verbose)?;
|
||||
|
||||
// change working directory to the parent directory of the exe
|
||||
let mut containing_dir = env::current_exe()?;
|
||||
@@ -173,7 +172,8 @@ fn patch(matches: &ArgMatches) -> Result<()> {
|
||||
info!(" Patch File: {:?}", patch_file);
|
||||
info!(" Output File: {:?}", output_file);
|
||||
|
||||
commands::patch(old_file, patch_file, output_file)
|
||||
velopack::delta::zstd_patch_single(old_file, patch_file, output_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply(matches: &ArgMatches) -> Result<()> {
|
||||
@@ -188,10 +188,10 @@ fn apply(matches: &ArgMatches) -> Result<()> {
|
||||
info!(" Package: {:?}", package);
|
||||
info!(" Exe Args: {:?}", exe_args);
|
||||
|
||||
let (root_path, app) = shared::detect_current_manifest()?;
|
||||
let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?;
|
||||
commands::apply(&root_path, &app, restart, wait, package, exe_args, true)
|
||||
let _mutex = shared::retry_io(|| windows::create_global_mutex(&locator.get_manifest_id()))?;
|
||||
commands::apply(&locator, restart, wait, package, exe_args, true)
|
||||
}
|
||||
|
||||
fn start(matches: &ArgMatches) -> Result<()> {
|
||||
@@ -209,17 +209,17 @@ fn start(matches: &ArgMatches) -> Result<()> {
|
||||
warn!("Legacy args format is deprecated and will be removed in a future release. Please update your application to use the new format.");
|
||||
}
|
||||
|
||||
let (root_path, app) = shared::detect_current_manifest()?;
|
||||
let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?;
|
||||
commands::start(&root_path, &app, wait, exe_name, exe_args, legacy_args)
|
||||
let _mutex = shared::retry_io(|| windows::create_global_mutex(&locator.get_manifest_id()))?;
|
||||
commands::start(&locator, wait, exe_name, exe_args, legacy_args)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn uninstall(_matches: &ArgMatches) -> Result<()> {
|
||||
info!("Command: Uninstall");
|
||||
let (root_path, app) = shared::detect_current_manifest()?;
|
||||
commands::uninstall(&root_path, &app, true)
|
||||
let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
|
||||
commands::uninstall(&locator, true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::dialogs::{self, DialogResult};
|
||||
use std::{ffi::OsStr, path::Path};
|
||||
use velopack::locator::VelopackLocator;
|
||||
|
||||
use crate::{bundle::Manifest, dialogs, dialogs::DialogResult};
|
||||
|
||||
pub fn close_processes_locking_dir(manifest: &Manifest, path: &str) -> bool {
|
||||
pub fn close_processes_locking_dir(locator: &VelopackLocator) -> bool {
|
||||
let app_title = locator.get_manifest_title();
|
||||
let app_version = locator.get_manifest_version_full_string();
|
||||
loop {
|
||||
let pids = filelocksmith::find_processes_locking_path(path);
|
||||
let bin_dir = locator.get_current_bin_dir();
|
||||
let pids = filelocksmith::find_processes_locking_path(&bin_dir);
|
||||
if pids.is_empty() {
|
||||
return true;
|
||||
}
|
||||
@@ -24,7 +27,7 @@ pub fn close_processes_locking_dir(manifest: &Manifest, path: &str) -> bool {
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
let result = dialogs::show_processes_locking_folder_dialog(manifest, &pids_str);
|
||||
let result = dialogs::show_processes_locking_folder_dialog(&app_title, &app_version, &pids_str);
|
||||
|
||||
match result {
|
||||
DialogResult::Retry => continue,
|
||||
@@ -41,8 +44,13 @@ pub fn close_processes_locking_dir(manifest: &Manifest, path: &str) -> bool {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_close_processes_locking_dir() {
|
||||
let mut mani = Manifest::default();
|
||||
let mut paths = velopack::locator::VelopackLocatorConfig::default();
|
||||
paths.CurrentBinaryDir = std::path::PathBuf::from(r"C:\Users\Caelan\AppData\Local\Clowd");
|
||||
|
||||
let mut mani = velopack::bundle::Manifest::default();
|
||||
mani.title = "Test".to_owned();
|
||||
mani.version = semver::Version::parse("1.0.0").unwrap();
|
||||
close_processes_locking_dir(&mani, r"C:\Users\Caelan\AppData\Local\Clowd");
|
||||
|
||||
let locator = VelopackLocator::new(paths.clone(), mani.clone());
|
||||
close_processes_locking_dir(&locator);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod runtimes;
|
||||
pub mod splash;
|
||||
pub mod known_path;
|
||||
pub mod strings;
|
||||
pub mod registry;
|
||||
|
||||
mod self_delete;
|
||||
mod shortcuts;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{runtimes, splash};
|
||||
use crate::shared::{bundle, dialogs, download};
|
||||
use crate::shared::dialogs;
|
||||
use velopack::{bundle, download};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
|
||||
53
src/bins/src/windows/registry.rs
Normal file
53
src/bins/src/windows/registry.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{Datelike, Local as DateTime};
|
||||
use velopack::locator::VelopackLocator;
|
||||
use winsafe::{self as w, co, prelude::*};
|
||||
|
||||
const UNINSTALL_REGISTRY_KEY: &'static str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
|
||||
|
||||
pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
|
||||
info!("Writing uninstall registry key...");
|
||||
|
||||
let app_id = locator.get_manifest_id();
|
||||
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 folder_size = fs_extra::dir::get_size(locator.get_root_dir()).unwrap_or(0);
|
||||
let short_version = locator.get_manifest_version_short_string();
|
||||
|
||||
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 = format!("\"{}\" --uninstall --silent", updater_path);
|
||||
|
||||
let reg_uninstall =
|
||||
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
|
||||
let reg_app = reg_uninstall.RegCreateKeyEx(&app_id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
|
||||
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
|
||||
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(app_title))?;
|
||||
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(short_version))?;
|
||||
reg_app.RegSetKeyValue(None, Some("InstallDate"), w::RegistryValue::Sz(formatted_date))?;
|
||||
reg_app.RegSetKeyValue(None, Some("InstallLocation"), w::RegistryValue::Sz(root_path_str))?;
|
||||
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(app_authors))?;
|
||||
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?;
|
||||
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?;
|
||||
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?;
|
||||
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?;
|
||||
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?;
|
||||
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
|
||||
info!("Removing uninstall registry keys...");
|
||||
let app_id = locator.get_manifest_id();
|
||||
let reg_uninstall =
|
||||
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
|
||||
reg_uninstall.RegDeleteKey(&app_id)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::shared as util;
|
||||
use crate::shared::download;
|
||||
use crate::shared::runtime_arch::RuntimeArch;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
use std::process::Command as Process;
|
||||
use std::{collections::HashMap, fs, path::Path};
|
||||
use velopack::download;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use winsafe::{self as w, co, prelude::*};
|
||||
|
||||
const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe";
|
||||
const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
|
||||
const REDIST_2015_2022_ARM64: &str = "https://aka.ms/vs/17/release/vc_redist.arm64.exe";
|
||||
|
||||
@@ -3,63 +3,30 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use bitflags::bitflags;
|
||||
use glob::glob;
|
||||
use same_file::is_same_file;
|
||||
use windows::core::{Interface, GUID, PCWSTR};
|
||||
use velopack::{locator::ShortcutLocationFlags, locator::VelopackLocator};
|
||||
use windows::core::{Interface, GUID, PCWSTR, PROPVARIANT};
|
||||
use windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID;
|
||||
use windows::Win32::System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, StructuredStorage::InitPropVariantFromStringVector, CLSCTX_ALL,
|
||||
COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, STGM_READWRITE,
|
||||
CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, StructuredStorage::InitPropVariantFromStringVector,
|
||||
CLSCTX_ALL, COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, STGM_READWRITE,
|
||||
};
|
||||
use windows::Win32::UI::Shell::{
|
||||
IShellItem, IShellLinkW, IStartMenuPinnedList, PropertiesSystem::IPropertyStore, SHCreateItemFromParsingName, ShellLink, StartMenuPin,
|
||||
};
|
||||
|
||||
use crate::bundle::Manifest;
|
||||
use crate::shared as util;
|
||||
use crate::windows::{known_path as known, strings::*};
|
||||
|
||||
// https://github.com/vaginessa/PWAsForFirefox/blob/fba68dbcc7ca27b970dc5a278ebdad32e0ab3c83/native/src/integrations/implementation/windows.rs#L28
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct ShortcutLocationFlags: u32 {
|
||||
const NONE = 0;
|
||||
const START_MENU = 1 << 0;
|
||||
const DESKTOP = 1 << 1;
|
||||
const STARTUP = 1 << 2;
|
||||
//const APP_ROOT = 1 << 3,
|
||||
const START_MENU_ROOT = 1 << 4;
|
||||
const USER_PINNED = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
impl ShortcutLocationFlags {
|
||||
fn from_string(input: &str) -> ShortcutLocationFlags {
|
||||
let mut flags = ShortcutLocationFlags::NONE;
|
||||
for part in input.split(|c| c == ',' || c == ';') {
|
||||
match part.trim().to_lowercase().as_str() {
|
||||
"none" => flags |= ShortcutLocationFlags::NONE,
|
||||
"startmenu" => flags |= ShortcutLocationFlags::START_MENU,
|
||||
"desktop" => flags |= ShortcutLocationFlags::DESKTOP,
|
||||
"startup" => flags |= ShortcutLocationFlags::STARTUP,
|
||||
"startmenuroot" => flags |= ShortcutLocationFlags::START_MENU_ROOT,
|
||||
_ => warn!("Warning: Unrecognized shortcut flag `{}`", part.trim()),
|
||||
}
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_or_update_manifest_lnks<P: AsRef<Path>>(root_path: P, next_app: &Manifest, previous_app: Option<&Manifest>) {
|
||||
let root_path = root_path.as_ref().to_owned().to_path_buf();
|
||||
pub fn create_or_update_manifest_lnks(next_app: &VelopackLocator, previous_app: Option<&VelopackLocator>) {
|
||||
let next_app = next_app.clone();
|
||||
let previous_app = previous_app.cloned();
|
||||
|
||||
unsafe {
|
||||
if let Err(e) = unsafe_run_delegate_in_com_context(move || {
|
||||
unsafe_update_app_manifest_lnks(&root_path, &next_app, previous_app.as_ref())?;
|
||||
unsafe_update_app_manifest_lnks(&next_app, previous_app.as_ref())?;
|
||||
Ok(())
|
||||
}) {
|
||||
warn!("Failed to update shortcuts: {}", e);
|
||||
@@ -84,59 +51,53 @@ unsafe fn create_instance<T: Interface>(clsid: &GUID) -> Result<T> {
|
||||
Ok(CoCreateInstance(clsid, None, CLSCTX_ALL)?)
|
||||
}
|
||||
|
||||
fn get_shortcut_filename(app: &Manifest) -> String {
|
||||
let name = if app.title.is_empty() { app.id.clone() } else { app.title.clone() };
|
||||
|
||||
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 shortcut_file_name = name + ".lnk";
|
||||
shortcut_file_name
|
||||
}
|
||||
|
||||
fn get_path_for_shortcut_location(app: &Manifest, flag: ShortcutLocationFlags) -> Result<PathBuf> {
|
||||
let shortcut_file_name = get_shortcut_filename(app);
|
||||
fn get_path_for_shortcut_location(app_id: &str, app_title: &str, app_author: &str, flag: ShortcutLocationFlags) -> Result<PathBuf> {
|
||||
let shortcut_file_name = get_shortcut_filename(app_id, app_title);
|
||||
match flag {
|
||||
ShortcutLocationFlags::DESKTOP => Ok(Path::new(&known::get_user_desktop()?).join(shortcut_file_name)),
|
||||
ShortcutLocationFlags::STARTUP => Ok(Path::new(&known::get_startup()?).join(shortcut_file_name)),
|
||||
ShortcutLocationFlags::START_MENU_ROOT => Ok(Path::new(&known::get_start_menu()?).join(shortcut_file_name)),
|
||||
ShortcutLocationFlags::START_MENU => {
|
||||
if app.authors.is_empty() {
|
||||
if app_author.is_empty() {
|
||||
warn!("No authors specified and START_MENU shortcut specified. Using START_MENU_ROOT instead.");
|
||||
Ok(Path::new(&known::get_start_menu()?).join(shortcut_file_name))
|
||||
} else {
|
||||
Ok(Path::new(&known::get_start_menu()?).join(&app.authors).join(shortcut_file_name))
|
||||
Ok(Path::new(&known::get_start_menu()?).join(app_author).join(shortcut_file_name))
|
||||
}
|
||||
}
|
||||
_ => bail!("Invalid shortcut location flag: {:?}", flag),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manifest, previous_app: Option<&Manifest>) -> Result<()> {
|
||||
let next_locations = ShortcutLocationFlags::from_string(&next_app.shortcut_locations);
|
||||
let prev_locations = if let Some(prev) = previous_app {
|
||||
ShortcutLocationFlags::from_string(&prev.shortcut_locations)
|
||||
} else {
|
||||
ShortcutLocationFlags::NONE
|
||||
};
|
||||
unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_app: Option<&VelopackLocator>) -> Result<()> {
|
||||
let next_locations = next_app.get_manifest_shortcut_locations();
|
||||
let prev_locations = previous_app.map(|a| a.get_manifest_shortcut_locations()).unwrap_or(ShortcutLocationFlags::NONE);
|
||||
|
||||
info!("Shortcut Previous Locations: {:?} ({:?})", prev_locations, previous_app.map(|a| a.version.clone()));
|
||||
info!("Shortcut Next Locations: {:?} ({:?})", next_locations, next_app.version);
|
||||
info!("Shortcut Previous Locations: {:?} ({:?})", prev_locations, previous_app.map(|a| a.get_manifest_version_full_string()));
|
||||
info!("Shortcut Next Locations: {:?} ({:?})", next_locations, next_app.get_manifest_version_full_string());
|
||||
|
||||
// we must end with shortcuts which exist in the next app but not the previous app.
|
||||
// any shortcuts which exist in both are optional - they could have been deleted by the user,
|
||||
// and we do not want to re-create them.
|
||||
let mut new_locations = next_locations - prev_locations - ShortcutLocationFlags::NONE;
|
||||
let mut new_locations = next_locations - prev_locations;
|
||||
|
||||
if new_locations.contains(ShortcutLocationFlags::START_MENU_ROOT) && new_locations.contains(ShortcutLocationFlags::START_MENU) {
|
||||
// if both start menu locations are specified, we prefer ROOT.
|
||||
new_locations.remove(ShortcutLocationFlags::START_MENU);
|
||||
}
|
||||
|
||||
let mut app_model_id: Option<String> = None;
|
||||
if !next_app.shortcut_amuid.is_empty() {
|
||||
app_model_id = Some(next_app.shortcut_amuid.clone());
|
||||
}
|
||||
|
||||
let app_main_exe = next_app.get_main_exe_path(root_path);
|
||||
let app_work_dir = next_app.get_current_path(root_path);
|
||||
let root_path = next_app.get_root_dir();
|
||||
let app_id = next_app.get_manifest_id();
|
||||
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();
|
||||
|
||||
info!("App Model ID: {:?}", app_model_id);
|
||||
let mut current_shortcuts = unsafe_get_shortcuts_for_root_dir(root_path)?;
|
||||
@@ -184,15 +145,15 @@ unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manife
|
||||
}
|
||||
|
||||
// rename existing shortcuts if packTitle has changed
|
||||
let last_app_name = previous_app.map(|a| a.title.clone()).unwrap_or(next_app.title.clone());
|
||||
let last_app_name = previous_app.map(|a| a.get_manifest_title()).unwrap_or(app_title.clone());
|
||||
let shortcuts_to_rename = unsafe_find_best_rename_candidates(&last_app_name, &app_main_exe, current_shortcuts);
|
||||
for (flag, path) in shortcuts_to_rename {
|
||||
let shortcut_file_name = get_shortcut_filename(next_app);
|
||||
let shortcut_file_name = get_shortcut_filename(&app_id, &app_title);
|
||||
|
||||
let target_path = if let Some(parent) = path.parent() {
|
||||
parent.join(shortcut_file_name)
|
||||
} else {
|
||||
get_path_for_shortcut_location(next_app, flag)?
|
||||
get_path_for_shortcut_location(&app_id, &app_title, &app_authors, flag)?
|
||||
};
|
||||
|
||||
if path != target_path {
|
||||
@@ -205,19 +166,17 @@ unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manife
|
||||
|
||||
// add new (missing) shortcut locations
|
||||
for flag in new_locations.iter() {
|
||||
let path = get_path_for_shortcut_location(next_app, flag)?;
|
||||
let target = next_app.get_main_exe_path(root_path);
|
||||
let work_dir = next_app.get_current_path(root_path);
|
||||
let path = get_path_for_shortcut_location(&app_id, &app_title, &app_authors, flag)?;
|
||||
info!("Creating new shortcut for flag '{:?}' ({:?}).", path, flag);
|
||||
|
||||
match Lnk::create_new() {
|
||||
Ok(mut lnk) => {
|
||||
if let Err(e) = lnk.set_target_path(&target) {
|
||||
if let Err(e) = lnk.set_target_path(&app_main_exe) {
|
||||
warn!("Failed to set target path: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
if let Err(e) = lnk.set_working_directory(&work_dir) {
|
||||
if let Err(e) = lnk.set_working_directory(&app_work_dir) {
|
||||
warn!("Failed to set working directory: {}", e);
|
||||
break;
|
||||
}
|
||||
@@ -227,7 +186,7 @@ unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manife
|
||||
break;
|
||||
}
|
||||
|
||||
if let Err(e) = lnk.set_icon_location(&target, 0) {
|
||||
if let Err(e) = lnk.set_icon_location(&app_main_exe, 0) {
|
||||
warn!("Failed to set icon location: {}", e);
|
||||
break;
|
||||
}
|
||||
@@ -514,16 +473,11 @@ impl Lnk {
|
||||
let id = PCWSTR(id.as_ptr());
|
||||
let variant = InitPropVariantFromStringVector(Some(&[id]))?;
|
||||
store.SetValue(&PKEY_AppUserModel_ID, &variant)?;
|
||||
store.Commit()?;
|
||||
} else {
|
||||
let prop_variant = PROPVARIANT::default(); // defaults to VT_EMPTY
|
||||
store.SetValue(&PKEY_AppUserModel_ID, &prop_variant)?;
|
||||
}
|
||||
// TODO: add clear/remove branch
|
||||
// else {
|
||||
// let mut varient = PROPVARIANT::default();
|
||||
// VariantToPropVariant(VT_EMPTY, &mut varient);
|
||||
// store.SetValue(&PKEY_AppUserModel_ID, VT_EMPTY)?;
|
||||
// VT_EMPTY
|
||||
// initpropvariant
|
||||
// }
|
||||
store.Commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -538,9 +492,6 @@ impl Lnk {
|
||||
Ok(self.pf.Save(output, true)?)
|
||||
}
|
||||
|
||||
// const SLR_NO_UI: u32 = 1;
|
||||
// const SLR_ANY_MATCH: u32 = 2;
|
||||
// const TIMEOUT_1MS: u32 = 1 << 16;
|
||||
|
||||
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();
|
||||
@@ -555,11 +506,16 @@ impl Lnk {
|
||||
|
||||
// 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
|
||||
|
||||
// const SLR_NO_UI: u32 = 1;
|
||||
// const SLR_ANY_MATCH: u32 = 2;
|
||||
// const TIMEOUT_1MS: u32 = 1 << 16;
|
||||
// let flags = Lnk::SLR_NO_UI | Lnk::SLR_ANY_MATCH | Lnk::TIMEOUT_1MS;
|
||||
// if let Err(e) = link.Resolve(HWND(0), flags) {
|
||||
// // this happens if the target path is missing and the link is broken
|
||||
// warn!("Failed to resolve link {} ({:?})", link_path, e);
|
||||
// }
|
||||
|
||||
Ok(Lnk { me: link, pf: persist, my_path: link_path })
|
||||
}
|
||||
|
||||
@@ -576,27 +532,6 @@ impl std::fmt::Debug for Lnk {
|
||||
}
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_shortcut_matching() {
|
||||
// let shortcuts = vec![
|
||||
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discor.lnk")),
|
||||
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discord Hello.lnk")),
|
||||
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discord.lnk")),
|
||||
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Hello Hello.lnk")),
|
||||
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\DiscordDa.lnk")),
|
||||
// (ShortcutLocationFlags::START_MENU_ROOT, PathBuf::from("C:\\Users\\Caelan\\Desktop\\asdasdasd3Discord2947230947kjsl.lnk")),
|
||||
// (ShortcutLocationFlags::START_MENU_ROOT, PathBuf::from("C:\\Users\\Caelan\\Desktop\\a2947230947kjsl.lnk")),
|
||||
// ];
|
||||
//
|
||||
// let matches = unsafe_find_best_shortcut_matches("Discord", shortcuts);
|
||||
// assert_eq!(matches.len(), 2);
|
||||
// assert_eq!(matches.get(&ShortcutLocationFlags::DESKTOP).unwrap(), &PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discord.lnk"));
|
||||
// assert_eq!(
|
||||
// matches.get(&ShortcutLocationFlags::START_MENU_ROOT).unwrap(),
|
||||
// &PathBuf::from("C:\\Users\\Caelan\\Desktop\\asdasdasd3Discord2947230947kjsl.lnk")
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_unpin_shortcut() {
|
||||
|
||||
@@ -5,6 +5,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use velopack::locator::VelopackLocator;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use normpath::PathExt;
|
||||
use wait_timeout::ChildExt;
|
||||
@@ -20,11 +22,12 @@ use windows::Win32::{
|
||||
use crate::shared::{self, runtime_arch::RuntimeArch};
|
||||
use crate::windows::strings::{string_to_u16, u16_to_string};
|
||||
|
||||
pub fn run_hook(app: &shared::bundle::Manifest, root_path: &PathBuf, hook_name: &str, timeout_secs: u64) -> bool {
|
||||
pub fn run_hook(locator: &VelopackLocator, hook_name: &str, timeout_secs: u64) -> bool {
|
||||
let sw = simple_stopwatch::Stopwatch::start_new();
|
||||
let current_path = app.get_current_path(&root_path);
|
||||
let main_exe_path = app.get_main_exe_path(&root_path);
|
||||
let ver_string = app.version.to_string();
|
||||
let root_dir = locator.get_root_dir();
|
||||
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 mut success = false;
|
||||
|
||||
@@ -59,7 +62,7 @@ pub fn run_hook(app: &shared::bundle::Manifest, root_path: &PathBuf, hook_name:
|
||||
}
|
||||
|
||||
// in case the hook left running processes
|
||||
let _ = shared::force_stop_package(&root_path);
|
||||
let _ = shared::force_stop_package(&root_dir);
|
||||
success
|
||||
}
|
||||
|
||||
@@ -75,8 +78,8 @@ impl Drop for MutexDropGuard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_global_mutex(app: &shared::bundle::Manifest) -> Result<MutexDropGuard> {
|
||||
let mutex_name = format!("velopack-{}", &app.id);
|
||||
pub fn create_global_mutex(app_id: &str) -> Result<MutexDropGuard> {
|
||||
let mutex_name = format!("velopack-{}", app_id);
|
||||
info!("Attempting to open global system mutex: '{}'", &mutex_name);
|
||||
let encodedu16 = super::strings::string_to_u16(mutex_name);
|
||||
let encoded = PCWSTR(encodedu16.as_ptr());
|
||||
|
||||
@@ -8,6 +8,8 @@ use velopack_bins::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use winsafe::{self as w, co};
|
||||
use velopack::bundle::load_bundle_from_file;
|
||||
use velopack::locator::{auto_locate_app_manifest, LocationContext};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
@@ -37,7 +39,8 @@ pub fn test_install_apply_uninstall() {
|
||||
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let tmp_buf = tmp_dir.path().to_path_buf();
|
||||
commands::install(Some(&nupkg), Some(&tmp_buf)).unwrap();
|
||||
let mut tmp_zip = load_bundle_from_file(nupkg).unwrap();
|
||||
commands::install(&mut tmp_zip, Some(&tmp_buf), None).unwrap();
|
||||
|
||||
assert!(!lnk_desktop_1.exists()); // desktop is created during update
|
||||
assert!(lnk_start_1.exists());
|
||||
@@ -46,13 +49,13 @@ pub fn test_install_apply_uninstall() {
|
||||
assert!(tmp_buf.join("current").join("AvaloniaCrossPlat.exe").exists());
|
||||
assert!(tmp_buf.join("current").join("sq.version").exists());
|
||||
|
||||
let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap();
|
||||
assert_eq!(app_id, app.id);
|
||||
assert_eq!(semver::Version::parse("1.0.11").unwrap(), app.version);
|
||||
let locator = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(tmp_buf.clone())).unwrap();
|
||||
assert_eq!(app_id, locator.get_manifest_id());
|
||||
assert_eq!(semver::Version::parse("1.0.11").unwrap(), locator.get_manifest_version());
|
||||
|
||||
let pkg_name_apply = "AvaloniaCrossPlat-1.0.15-win-full.nupkg";
|
||||
let nupkg_apply = fixtures.join(pkg_name_apply);
|
||||
commands::apply(&root_dir, &app, false, shared::OperationWait::NoWait, Some(&nupkg_apply), None, false).unwrap();
|
||||
commands::apply(&locator, false, shared::OperationWait::NoWait, Some(&nupkg_apply), None, false).unwrap();
|
||||
|
||||
// shortcuts are renamed, and desktop is created
|
||||
assert!(!lnk_desktop_1.exists());
|
||||
@@ -60,10 +63,10 @@ pub fn test_install_apply_uninstall() {
|
||||
assert!(lnk_desktop_2.exists());
|
||||
assert!(lnk_start_2.exists());
|
||||
|
||||
let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap();
|
||||
assert_eq!(semver::Version::parse("1.0.15").unwrap(), app.version);
|
||||
let locator = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(tmp_buf.clone())).unwrap();
|
||||
assert_eq!(semver::Version::parse("1.0.15").unwrap(), locator.get_manifest_version());
|
||||
|
||||
commands::uninstall(&root_dir, &app, false).unwrap();
|
||||
commands::uninstall(&locator, false).unwrap();
|
||||
assert!(!tmp_buf.join("current").exists());
|
||||
assert!(tmp_buf.join(".dead").exists());
|
||||
|
||||
@@ -83,7 +86,9 @@ pub fn test_install_preserve_symlinks() {
|
||||
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let tmp_buf = tmp_dir.path().to_path_buf();
|
||||
commands::install(Some(&nupkg), Some(&tmp_buf)).unwrap();
|
||||
let mut tmp_zip = load_bundle_from_file(nupkg).unwrap();
|
||||
|
||||
commands::install(&mut tmp_zip, Some(&tmp_buf), None).unwrap();
|
||||
|
||||
assert!(tmp_buf.join("current").join("actual").join("file.txt").exists());
|
||||
assert!(tmp_buf.join("current").join("other").join("syml").exists());
|
||||
@@ -112,14 +117,14 @@ pub fn test_patch_apply() {
|
||||
}
|
||||
|
||||
let expected_sha1 = get_sha1(&new_file);
|
||||
let tmp_file = std::path::Path::new("temp.patch").to_path_buf();
|
||||
let tmp_file = Path::new("temp.patch").to_path_buf();
|
||||
|
||||
commands::patch(&old_file, &p1, &tmp_file).unwrap();
|
||||
velopack::delta::zstd_patch_single(&old_file, &p1, &tmp_file).unwrap();
|
||||
let tmp_sha1 = get_sha1(&tmp_file);
|
||||
fs::remove_file(&tmp_file).unwrap();
|
||||
assert_eq!(expected_sha1, tmp_sha1);
|
||||
|
||||
commands::patch(&old_file, &p2, &tmp_file).unwrap();
|
||||
velopack::delta::zstd_patch_single(&old_file, &p2, &tmp_file).unwrap();
|
||||
let tmp_sha1 = get_sha1(&tmp_file);
|
||||
fs::remove_file(&tmp_file).unwrap();
|
||||
assert_eq!(expected_sha1, tmp_sha1);
|
||||
|
||||
@@ -15,8 +15,8 @@ serde_json = "1"
|
||||
velopack = { path = "../../../lib-rust" }
|
||||
semver = "1.0"
|
||||
log = "0.4"
|
||||
lazy_static = "1.4"
|
||||
lazy_static = "1.5"
|
||||
|
||||
[build-dependencies]
|
||||
ts-rs = "9"
|
||||
ts-rs = "10.0"
|
||||
velopack = { path = "../../../lib-rust", features = ["typescript"] }
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{env, path::Path};
|
||||
use locator::VelopackLocator;
|
||||
use locator::VelopackLocatorConfig;
|
||||
use ts_rs::TS;
|
||||
use velopack::*;
|
||||
|
||||
@@ -8,5 +8,5 @@ fn main() {
|
||||
let bindings_dir = Path::new(&manifest_dir).join("..").join("..").join("src").join("bindings");
|
||||
UpdateInfo::export_all_to(&bindings_dir).unwrap();
|
||||
UpdateOptions::export_all_to(&bindings_dir).unwrap();
|
||||
VelopackLocator::export_all_to(&bindings_dir).unwrap();
|
||||
VelopackLocatorConfig::export_all_to(&bindings_dir).unwrap();
|
||||
}
|
||||
@@ -16,14 +16,14 @@ struct UpdateManagerWrapper {
|
||||
impl Finalize for UpdateManagerWrapper {}
|
||||
type BoxedUpdateManager = JsBox<RefCell<UpdateManagerWrapper>>;
|
||||
|
||||
fn args_get_locator(cx: &mut FunctionContext, i: usize) -> NeonResult<Option<VelopackLocator>> {
|
||||
fn args_get_locator(cx: &mut FunctionContext, i: usize) -> NeonResult<Option<VelopackLocatorConfig>> {
|
||||
let arg_locator = cx.argument_opt(i);
|
||||
if let Some(js_value) = arg_locator {
|
||||
if js_value.is_a::<JsString, _>(cx) {
|
||||
if let Ok(js_string) = js_value.downcast::<JsString, _>(cx) {
|
||||
let arg_locator = js_string.value(cx);
|
||||
if !arg_locator.is_empty() {
|
||||
let locator = serde_json::from_str::<VelopackLocator>(&arg_locator).or_else(|e| cx.throw_error(e.to_string()))?;
|
||||
let locator = serde_json::from_str::<VelopackLocatorConfig>(&arg_locator).or_else(|e| cx.throw_error(e.to_string()))?;
|
||||
return Ok(Some(locator));
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,7 @@ fn js_appbuilder_run(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
|
||||
let undefined = cx.undefined();
|
||||
let cx_ref = Rc::new(RefCell::new(cx));
|
||||
let cx_ref2 = cx_ref.clone();
|
||||
|
||||
let hook_handler = move |hook_name: &str, current_version: Version| {
|
||||
let mut cx = cx_ref.borrow_mut();
|
||||
@@ -231,7 +232,10 @@ fn js_appbuilder_run(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
builder = builder.set_args(argarray);
|
||||
}
|
||||
|
||||
builder.run();
|
||||
builder.run().or_else(|e| {
|
||||
let mut cx = cx_ref2.borrow_mut();
|
||||
cx.throw_error(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(undefined)
|
||||
}
|
||||
|
||||
30
src/lib-nodejs/src/bindings/VelopackLocatorConfig.ts
Normal file
30
src/lib-nodejs/src/bindings/VelopackLocatorConfig.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
|
||||
*/
|
||||
export type VelopackLocatorConfig = {
|
||||
/**
|
||||
* The root directory of the current app.
|
||||
*/
|
||||
RootAppDir: string,
|
||||
/**
|
||||
* The path to the Update.exe binary.
|
||||
*/
|
||||
UpdateExePath: string,
|
||||
/**
|
||||
* The path to the packages' directory.
|
||||
*/
|
||||
PackagesDir: string,
|
||||
/**
|
||||
* The current app manifest.
|
||||
*/
|
||||
ManifestPath: string,
|
||||
/**
|
||||
* The directory containing the application's user binaries.
|
||||
*/
|
||||
CurrentBinaryDir: string,
|
||||
/**
|
||||
* Whether the current application is portable or installed.
|
||||
*/
|
||||
IsPortable: boolean, };
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
|
||||
*/
|
||||
export type VelopackLocator = {
|
||||
export type VelopackPaths = {
|
||||
/**
|
||||
* The root directory of the current app.
|
||||
*/
|
||||
@@ -21,6 +21,6 @@ PackagesDir: string,
|
||||
*/
|
||||
ManifestPath: string,
|
||||
/**
|
||||
* The temporary directory for the current app.
|
||||
* The directory containing the application's user binaries
|
||||
*/
|
||||
TempDir: string, };
|
||||
CurrentBinaryDir: string, };
|
||||
@@ -32,9 +32,13 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
zip = { version = "2.1", default-features = false, features = ["deflate"] }
|
||||
thiserror = "1.0"
|
||||
lazy_static = "1.5"
|
||||
regex = "1.10"
|
||||
normpath = "1.2"
|
||||
bitflags = "2.6"
|
||||
|
||||
# typescript
|
||||
ts-rs = { version = "9", optional = true }
|
||||
ts-rs = { version = "10.0", optional = true }
|
||||
|
||||
# delta packages
|
||||
zstd = { version = "0.13", optional = true }
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::env;
|
||||
use std::process::exit;
|
||||
|
||||
use crate::{
|
||||
locator::{auto_locate, VelopackLocator},
|
||||
locator::{auto_locate_app_manifest, VelopackLocatorConfig},
|
||||
Error,
|
||||
};
|
||||
use crate::locator::{LocationContext, VelopackLocator};
|
||||
|
||||
/// VelopackApp helps you to handle app activation events correctly.
|
||||
/// This should be used as early as possible in your application startup code.
|
||||
@@ -19,7 +20,7 @@ pub struct VelopackApp<'a> {
|
||||
restarted_hook: Option<Box<dyn FnOnce(Version) + 'a>>,
|
||||
// auto_apply: bool,
|
||||
args: Vec<String>,
|
||||
locator: Option<VelopackLocator>,
|
||||
locator: Option<VelopackLocatorConfig>,
|
||||
}
|
||||
|
||||
impl<'a> VelopackApp<'a> {
|
||||
@@ -51,7 +52,7 @@ impl<'a> VelopackApp<'a> {
|
||||
// }
|
||||
|
||||
/// Override the default file locator with a custom one (eg. for testing)
|
||||
pub fn set_locator(mut self, locator: VelopackLocator) -> Self {
|
||||
pub fn set_locator(mut self, locator: VelopackLocatorConfig) -> Self {
|
||||
self.locator = Some(locator);
|
||||
self
|
||||
}
|
||||
@@ -110,7 +111,7 @@ impl<'a> VelopackApp<'a> {
|
||||
|
||||
/// Runs the Velopack startup logic. This should be the first thing to run in your app.
|
||||
/// In some circumstances it may terminate/restart the process to perform tasks.
|
||||
pub fn run(&mut self) {
|
||||
pub fn run(&mut self) -> Result<(), Error> {
|
||||
let args: Vec<String> = self.args.clone();
|
||||
|
||||
info!("VelopackApp: Running with args: {:?}", args);
|
||||
@@ -125,14 +126,15 @@ impl<'a> VelopackApp<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let my_version = match self.get_current_version() {
|
||||
Ok(ver) => ver,
|
||||
Err(e) => {
|
||||
warn!("VelopackApp: Error getting current version: {}", e);
|
||||
semver::Version::new(0, 0, 0)
|
||||
}
|
||||
let locator = if let Some(config) = &self.locator {
|
||||
let manifest = config.load_manifest()?;
|
||||
VelopackLocator::new(config.clone(), manifest)
|
||||
} else {
|
||||
auto_locate_app_manifest(LocationContext::FromCurrentExe)?
|
||||
};
|
||||
|
||||
let my_version = locator.get_manifest_version();
|
||||
|
||||
let firstrun = env::var("VELOPACK_FIRSTRUN").is_ok();
|
||||
let restarted = env::var("VELOPACK_RESTART").is_ok();
|
||||
env::remove_var("VELOPACK_FIRSTRUN");
|
||||
@@ -145,6 +147,8 @@ impl<'a> VelopackApp<'a> {
|
||||
if restarted {
|
||||
Self::call_hook(&mut self.restarted_hook, &my_version);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn call_hook(hook_option: &mut Option<Box<dyn FnOnce(Version) + 'a>>, version: &Version) {
|
||||
@@ -164,17 +168,4 @@ impl<'a> VelopackApp<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_locator(&self) -> Result<VelopackLocator, Error> {
|
||||
if let Some(locator) = &self.locator {
|
||||
return Ok(locator.clone());
|
||||
}
|
||||
|
||||
let exe_path = env::current_exe()?;
|
||||
auto_locate(exe_path)
|
||||
}
|
||||
|
||||
fn get_current_version(&self) -> Result<Version, Error> {
|
||||
self.get_locator().map(|l| l.load_manifest()).map(|m| m.map(|m| m.version))?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs::{self, File},
|
||||
@@ -5,10 +7,18 @@ 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 zip::ZipArchive;
|
||||
|
||||
use crate::{Error, manifest::*, util};
|
||||
use crate::{Error, util};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use normpath::PathExt;
|
||||
|
||||
pub trait ReadSeek: Read + Seek {}
|
||||
|
||||
@@ -17,20 +27,52 @@ impl<T: Read + Seek> ReadSeek for T {}
|
||||
#[derive(Clone)]
|
||||
pub struct BundleZip<'a> {
|
||||
zip: Rc<RefCell<ZipArchive<Box<dyn ReadSeek + 'a>>>>,
|
||||
zip_from_file: bool,
|
||||
zip_range: Option<&'a [u8]>,
|
||||
file_path: Option<PathBuf>,
|
||||
manifest: Option<Manifest>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
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());
|
||||
let file = util::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)) })
|
||||
Ok(BundleZip {
|
||||
zip: Rc::new(RefCell::new(zip)),
|
||||
zip_from_file: true,
|
||||
file_path: Some(file_name.to_owned()),
|
||||
zip_range: None,
|
||||
manifest: None,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl BundleZip<'_> {
|
||||
pub fn copy_bundle_to_file<T: AsRef<Path>>(&self, output_file_path: T) -> Result<(), Error> {
|
||||
let nupkg_path = output_file_path.as_ref();
|
||||
if self.zip_from_file {
|
||||
util::retry_io(|| fs::copy(self.file_path.clone().unwrap(), nupkg_path))?;
|
||||
} else {
|
||||
util::retry_io(|| fs::write(nupkg_path, self.zip_range.unwrap()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn calculate_size(&self) -> (u64, u64) {
|
||||
let mut total_uncompressed_size = 0u64;
|
||||
let mut total_compressed_size = 0u64;
|
||||
@@ -78,7 +120,8 @@ impl BundleZip<'_> {
|
||||
}
|
||||
|
||||
pub fn find_zip_file<F>(&self, predicate: F) -> Option<usize>
|
||||
where F: Fn(&str) -> bool,
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
for i in 0..archive.len() {
|
||||
@@ -121,7 +164,8 @@ impl BundleZip<'_> {
|
||||
}
|
||||
|
||||
pub fn extract_zip_predicate_to_path<F, T: AsRef<Path>>(&self, predicate: F, path: T) -> Result<usize, Error>
|
||||
where F: Fn(&str) -> bool,
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
let idx = self.find_zip_file(predicate);
|
||||
if idx.is_none() {
|
||||
@@ -132,7 +176,11 @@ impl BundleZip<'_> {
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
pub fn read_manifest(&self) -> Result<Manifest, Error> {
|
||||
pub fn read_manifest(&mut self) -> Result<Manifest, Error> {
|
||||
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)?;
|
||||
|
||||
@@ -140,6 +188,8 @@ impl BundleZip<'_> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -158,4 +208,317 @@ impl BundleZip<'_> {
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn create_symlink(link_path: &PathBuf, target_path: &PathBuf) -> Result<(), Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
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(),
|
||||
absolute_path.is_file(),
|
||||
absolute_path.is_dir(),
|
||||
target_path.to_string_lossy()
|
||||
);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
std::os::unix::fs::symlink(target_path, link_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn extract_lib_contents_to_path<P: AsRef<Path>, F: Fn(i16)>(&self, current_path: P, progress: F) -> Result<(), Error> {
|
||||
let current_path = current_path.as_ref();
|
||||
let files = self.get_file_names()?;
|
||||
let num_files = files.len();
|
||||
|
||||
info!("Extracting {} app files to '{}'...", num_files, current_path.to_string_lossy());
|
||||
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
|
||||
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
|
||||
let symlink_regex = Regex::new(".__symlink$").unwrap();
|
||||
let updater_idx = self.find_zip_file(|name| name.ends_with("Squirrel.exe"));
|
||||
|
||||
// for legacy support, we still extract the nuspec file to the current dir.
|
||||
// in newer versions, the nuspec is in the current dir in the package itself.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
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)?;
|
||||
}
|
||||
|
||||
// we extract the symlinks after, because the target must exist.
|
||||
let mut symlinks: Vec<(usize, PathBuf)> = Vec::new();
|
||||
|
||||
for (i, key) in files.iter().enumerate() {
|
||||
if Some(i) == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") {
|
||||
debug!(" {} Skipped '{}'", i, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path_in_zip = re.replace(key, "").to_string();
|
||||
let file_path_on_disk = Path::new(¤t_path).join(&file_path_in_zip);
|
||||
|
||||
if symlink_regex.is_match(&file_path_in_zip) {
|
||||
let sym_key = symlink_regex.replace(&file_path_in_zip, "").to_string();
|
||||
let file_path_on_disk = Path::new(¤t_path).join(&sym_key);
|
||||
symlinks.push((i, file_path_on_disk));
|
||||
continue;
|
||||
}
|
||||
|
||||
if stub_regex.is_match(&file_path_in_zip) {
|
||||
// let stub_key = stub_regex.replace(&file_path_in_zip, ".exe").to_string();
|
||||
// file_path_on_disk = root_path.join(&stub_key);
|
||||
debug!(" {} Skipped Stub (obsolete) '{}'", i, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// on windows, the zip paths are / and should be \ instead
|
||||
#[cfg(target_os = "windows")]
|
||||
let file_path_on_disk = file_path_on_disk.normalize_virtually()?;
|
||||
#[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());
|
||||
self.extract_zip_idx_to_path(i, &file_path_on_disk)?;
|
||||
|
||||
// on macos, we need to chmod +x the executable files
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(true) = super::macho::is_macho_image(&file_path_on_disk) {
|
||||
if let Err(e) = std::fs::set_permissions(&file_path_on_disk, std::fs::Permissions::from_mode(0o755)) {
|
||||
warn!("Failed to set executable permissions on '{}': {}", file_path_on_disk.to_string_lossy(), e);
|
||||
} else {
|
||||
info!(" {} Set executable permissions on '{}'", i, file_path_on_disk.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress(((i as f32 / num_files as f32) * 100.0) as i16);
|
||||
}
|
||||
|
||||
// we extract the symlinks after, because the target must exist.
|
||||
for (i, link_path) in symlinks {
|
||||
let mut archive = self.zip.borrow_mut();
|
||||
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);
|
||||
|
||||
let contents = contents.trim_end_matches('/');
|
||||
#[cfg(target_os = "windows")]
|
||||
let contents = contents.replace("/", "\\");
|
||||
let contents = PathBuf::from(contents);
|
||||
|
||||
let parent = link_path.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
debug!("Creating parent directory: {:?}", parent);
|
||||
util::retry_io(|| fs::create_dir_all(parent))?;
|
||||
}
|
||||
util::retry_io(|| Self::create_symlink(&link_path, &contents))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, derivative::Derivative, Clone)]
|
||||
#[derivative(Default)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Manifest {
|
||||
pub id: String,
|
||||
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
|
||||
pub version: Version,
|
||||
pub title: String,
|
||||
pub authors: String,
|
||||
pub description: String,
|
||||
pub machine_architecture: String,
|
||||
pub runtime_dependencies: String,
|
||||
pub main_exe: String,
|
||||
pub os: String,
|
||||
pub os_min_version: String,
|
||||
pub channel: String,
|
||||
pub shortcut_locations: String,
|
||||
pub shortcut_amuid: String,
|
||||
}
|
||||
|
||||
/// Parse manifest object from an XML string.
|
||||
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest, Error> {
|
||||
let mut obj: Manifest = Default::default();
|
||||
let cursor = Cursor::new(xml);
|
||||
let parser = EventReader::new(cursor);
|
||||
let mut vec: Vec<String> = Vec::new();
|
||||
for e in parser {
|
||||
match e {
|
||||
Ok(XmlEvent::StartElement { name, .. }) => {
|
||||
vec.push(name.local_name);
|
||||
}
|
||||
Ok(XmlEvent::Characters(text)) => {
|
||||
if vec.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let el_name = vec.last().unwrap();
|
||||
if el_name == "id" {
|
||||
obj.id = text;
|
||||
} else if el_name == "version" {
|
||||
obj.version = Version::parse(&text)?;
|
||||
} else if el_name == "title" {
|
||||
obj.title = text;
|
||||
} else if el_name == "authors" {
|
||||
obj.authors = text;
|
||||
} else if el_name == "description" {
|
||||
obj.description = text;
|
||||
} else if el_name == "machineArchitecture" {
|
||||
obj.machine_architecture = text;
|
||||
} else if el_name == "runtimeDependencies" {
|
||||
obj.runtime_dependencies = text;
|
||||
} else if el_name == "mainExe" {
|
||||
obj.main_exe = text;
|
||||
} else if el_name == "os" {
|
||||
obj.os = text;
|
||||
} else if el_name == "osMinVersion" {
|
||||
obj.os_min_version = text;
|
||||
} else if el_name == "channel" {
|
||||
obj.channel = text;
|
||||
} else if el_name == "shortcutLocations" {
|
||||
obj.shortcut_locations = text;
|
||||
} else if el_name == "shortcutAmuid" {
|
||||
obj.shortcut_amuid = text;
|
||||
}
|
||||
}
|
||||
Ok(XmlEvent::EndElement { .. }) => {
|
||||
vec.pop();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error: {e}");
|
||||
break;
|
||||
}
|
||||
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.id.is_empty() {
|
||||
return Err(Error::MissingNuspecProperty("id".to_owned()));
|
||||
}
|
||||
|
||||
if obj.version == Version::new(0, 0, 0) {
|
||||
return Err(Error::MissingNuspecProperty("version".to_owned()));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if obj.main_exe.is_empty() {
|
||||
return Err(Error::MissingNuspecProperty("mainExe".to_owned()));
|
||||
}
|
||||
|
||||
if obj.title.is_empty() {
|
||||
obj.title = obj.id.clone();
|
||||
}
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derivative::Derivative)]
|
||||
#[derivative(Default)]
|
||||
pub struct EntryNameInfo {
|
||||
pub name: String,
|
||||
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
|
||||
pub version: Version,
|
||||
pub is_delta: bool,
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
impl EntryNameInfo {
|
||||
pub fn load_manifest(&self) -> Result<Manifest, Error> {
|
||||
let path = Path::new(&self.file_path).to_path_buf();
|
||||
let mut bundle = load_bundle_from_file(&path)?;
|
||||
bundle.read_manifest()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENTRY_SUFFIX_FULL: Regex = Regex::new(r"(?i)-full.nupkg$").unwrap();
|
||||
static ref ENTRY_SUFFIX_DELTA: Regex = Regex::new(r"(?i)-delta.nupkg$").unwrap();
|
||||
static ref ENTRY_VERSION_START: Regex = Regex::new(r"[\.-](0|[1-9]\d*)\.(0|[1-9]\d*)($|[^\d])").unwrap();
|
||||
}
|
||||
|
||||
/// Parse a package file path into an EntryNameInfo object. Returns None if couldn't be parsed.
|
||||
pub fn parse_package_file_path<P: AsRef<Path>>(path: P) -> Option<EntryNameInfo> {
|
||||
let path = path.as_ref();
|
||||
let name = path.file_name()?.to_string_lossy().to_string();
|
||||
let m = parse_package_file_name(name);
|
||||
if m.is_some() {
|
||||
let mut m = m.unwrap();
|
||||
m.file_path = path.to_string_lossy().to_string();
|
||||
return Some(m);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn parse_package_file_name<T: AsRef<str>>(name: T) -> Option<EntryNameInfo> {
|
||||
let name = name.as_ref();
|
||||
let full = ENTRY_SUFFIX_FULL.is_match(name);
|
||||
let delta = ENTRY_SUFFIX_DELTA.is_match(name);
|
||||
if !full && !delta {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 ver_idx = ENTRY_VERSION_START.find(&name_and_ver);
|
||||
if ver_idx.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ver_idx = ver_idx.unwrap().start();
|
||||
entry.name = name_and_ver[0..ver_idx].to_string();
|
||||
let ver_idx = ver_idx + 1;
|
||||
let version = name_and_ver[ver_idx..].to_string();
|
||||
|
||||
let sv = Version::parse(&version);
|
||||
if sv.is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
entry.version = sv.unwrap();
|
||||
return Some(entry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_package_file_name() {
|
||||
// test no rid
|
||||
let entry = parse_package_file_name("Velopack-1.0.0-full.nupkg").unwrap();
|
||||
assert_eq!(entry.name, "Velopack");
|
||||
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
|
||||
assert_eq!(entry.is_delta, false);
|
||||
|
||||
let entry = parse_package_file_name("Velopack-1.0.0-delta.nupkg").unwrap();
|
||||
assert_eq!(entry.name, "Velopack");
|
||||
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
|
||||
assert_eq!(entry.is_delta, true);
|
||||
|
||||
let entry = parse_package_file_name("My.Cool-App-1.1.0-full.nupkg").unwrap();
|
||||
assert_eq!(entry.name, "My.Cool-App");
|
||||
assert_eq!(entry.version, Version::parse("1.1.0").unwrap());
|
||||
assert_eq!(entry.is_delta, false);
|
||||
|
||||
// test invalid names
|
||||
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.nupkg.zip").is_none());
|
||||
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());
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use std::io::{Read, Write};
|
||||
|
||||
use crate::{Error, util};
|
||||
|
||||
/// 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>
|
||||
where A: FnMut(i16),
|
||||
{
|
||||
@@ -39,6 +40,7 @@ pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads a file from a URL and returns it as a string.
|
||||
pub fn download_url_as_string(url: &str) -> Result<String, Error> {
|
||||
let agent = get_download_agent()?;
|
||||
let r = agent.get(url).call()?.into_string()?;
|
||||
|
||||
@@ -43,16 +43,14 @@
|
||||
//!
|
||||
//! fn update_my_app() {
|
||||
//! let source = sources::HttpSource::new("https://the.place/you-host/updates");
|
||||
//! let um = UpdateManager::new(source, None).unwrap();
|
||||
//! let um = UpdateManager::new(source, None, None).unwrap();
|
||||
//!
|
||||
//! if let UpdateCheck::UpdateAvailable(updates) = um.check_for_updates().unwrap() {
|
||||
//! // there was an update available. Download it.
|
||||
//! um.download_updates(&updates, |progress| {
|
||||
//! println!("Download progress: {}%", progress);
|
||||
//! }).unwrap();
|
||||
//! um.download_updates(&updates, None).unwrap();
|
||||
//!
|
||||
//! // download completed, let's restart and update
|
||||
//! um.apply_updates_and_restart(&updates, RestartArgs::None).unwrap();
|
||||
//! um.apply_updates_and_restart(&updates).unwrap();
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
@@ -66,7 +64,7 @@
|
||||
//! ```sh
|
||||
//! dotnet tool update -g vpk
|
||||
//! ```
|
||||
//! ***Note: you must have the .NET Core SDK 6 installed to use and update `vpk`***
|
||||
//! ***Note: you must have the .NET Core SDK 8 installed to use and update `vpk`***
|
||||
//!
|
||||
//! 6. Package your Velopack release / installers:
|
||||
//! ```sh
|
||||
@@ -81,12 +79,15 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod app;
|
||||
mod bundle;
|
||||
mod download;
|
||||
mod manager;
|
||||
mod manifest;
|
||||
mod util;
|
||||
|
||||
/// Utility functions for loading and working with Velopack bundles and manifests.
|
||||
pub mod bundle;
|
||||
|
||||
/// Utility function for downloading files with progress reporting.
|
||||
pub mod download;
|
||||
|
||||
/// Locator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
|
||||
pub mod locator;
|
||||
|
||||
@@ -138,6 +139,8 @@ pub enum Error
|
||||
MissingUpdateExe,
|
||||
#[error("This application is not properly installed: {0}")]
|
||||
NotInstalled(String),
|
||||
#[error("Generic error: {0}")]
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use semver::Version;
|
||||
use crate::{
|
||||
manifest::{self, Manifest},
|
||||
bundle::{self, Manifest},
|
||||
util, Error,
|
||||
};
|
||||
|
||||
@@ -16,15 +16,15 @@ pub fn default_channel_name() -> String {
|
||||
}
|
||||
|
||||
/// Default log location for Velopack on the current OS.
|
||||
pub fn default_log_location() -> PathBuf {
|
||||
pub fn default_log_location(context: LocationContext) -> PathBuf {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut my_exe = std::env::current_exe().expect("Could not locate current executable");
|
||||
if let Ok(locator) = auto_locate(&my_exe) {
|
||||
return locator.RootAppDir.join("Velopack.log");
|
||||
if let Ok(locator) = auto_locate_app_manifest(context) {
|
||||
return locator.get_root_dir().join("Velopack.log");
|
||||
}
|
||||
|
||||
// If we can't locate the current app, we write to the parent directory.
|
||||
let mut my_exe = std::env::current_exe().expect("Could not locate current executable");
|
||||
my_exe.pop();
|
||||
my_exe.pop();
|
||||
return my_exe.join("Velopack.log");
|
||||
@@ -44,74 +44,354 @@ pub fn default_log_location() -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// ShortcutLocationFlags is a bitflags enumeration of system shortcut locations.
|
||||
pub struct ShortcutLocationFlags: u32 {
|
||||
/// No shortcut.
|
||||
const NONE = 0;
|
||||
/// Start Menu shortcut inside a PackAuthor folder.
|
||||
const START_MENU = 1 << 0;
|
||||
/// Desktop shortcut.
|
||||
const DESKTOP = 1 << 1;
|
||||
/// Startup shortcut.
|
||||
const STARTUP = 1 << 2;
|
||||
//const APP_ROOT = 1 << 3,
|
||||
/// Start Menu shortcut at the root level (not inside an author/publisher folder).
|
||||
const START_MENU_ROOT = 1 << 4;
|
||||
/// User pinned to taskbar shortcut.
|
||||
const USER_PINNED = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
impl ShortcutLocationFlags {
|
||||
/// Parses a string containing comma or semicolon delimited shortcut flags.
|
||||
pub fn from_string(input: &str) -> ShortcutLocationFlags {
|
||||
let mut flags = ShortcutLocationFlags::NONE;
|
||||
for part in input.split(|c| c == ',' || c == ';') {
|
||||
match part.trim().to_lowercase().as_str() {
|
||||
"none" => flags |= ShortcutLocationFlags::NONE,
|
||||
"startmenu" => flags |= ShortcutLocationFlags::START_MENU,
|
||||
"desktop" => flags |= ShortcutLocationFlags::DESKTOP,
|
||||
"startup" => flags |= ShortcutLocationFlags::STARTUP,
|
||||
"startmenuroot" => flags |= ShortcutLocationFlags::START_MENU_ROOT,
|
||||
_ => warn!("Warning: Unrecognized shortcut flag `{}`", part.trim()),
|
||||
}
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
/// VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct VelopackLocator {
|
||||
pub struct VelopackLocatorConfig {
|
||||
/// The root directory of the current app.
|
||||
pub RootAppDir: PathBuf,
|
||||
/// The path to the Update.exe binary.
|
||||
pub UpdateExePath: PathBuf,
|
||||
/// The path to the packages directory.
|
||||
/// The path to the packages' directory.
|
||||
pub PackagesDir: PathBuf,
|
||||
/// The current app manifest.
|
||||
pub ManifestPath: PathBuf,
|
||||
/// The temporary directory for the current app.
|
||||
pub TempDir: PathBuf,
|
||||
/// The directory containing the application's user binaries.
|
||||
pub CurrentBinaryDir: PathBuf,
|
||||
/// Whether the current application is portable or installed.
|
||||
pub IsPortable: bool,
|
||||
}
|
||||
|
||||
impl VelopackLocator {
|
||||
impl VelopackLocatorConfig {
|
||||
/// Load and parse the current app manifest from the manifest_path field. This will return an error if the manifest is missing.
|
||||
pub fn load_manifest(&self) -> Result<Manifest, Error> {
|
||||
read_current_manifest(&self.ManifestPath)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
|
||||
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error> {
|
||||
// check if Update.exe exists in parent dir, if it does, that's the root dir.
|
||||
let mut path = exe_path.as_ref().to_path_buf();
|
||||
path.pop(); // current dir
|
||||
path.pop(); // root dir
|
||||
if path.join("Update.exe").exists() {
|
||||
info!("Found Update.exe in parent directory: {}", path.to_string_lossy());
|
||||
return Ok(VelopackLocator {
|
||||
RootAppDir: path.clone(),
|
||||
UpdateExePath: path.join("Update.exe"),
|
||||
PackagesDir: path.join("packages"),
|
||||
ManifestPath: path.join("current").join("sq.version"),
|
||||
TempDir: path.join("packages").join("VelopackTemp"),
|
||||
});
|
||||
/// VelopackLocator provides some utility functions for locating the current app important paths
|
||||
#[derive(Clone)]
|
||||
pub struct VelopackLocator {
|
||||
paths: VelopackLocatorConfig,
|
||||
manifest: Manifest,
|
||||
}
|
||||
|
||||
impl VelopackLocator {
|
||||
/// Creates a new VelopackLocator from the given paths and manifest.
|
||||
pub fn new(paths: VelopackLocatorConfig, manifest: Manifest) -> Self {
|
||||
Self { paths, manifest }
|
||||
}
|
||||
|
||||
// see if we can find the current dir in the path, maybe we're more nested than that.
|
||||
path = exe_path.as_ref().to_path_buf();
|
||||
let path = path.to_string_lossy();
|
||||
/// Returns the path to the current app's packages directory.
|
||||
pub fn get_packages_dir(&self) -> PathBuf {
|
||||
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);
|
||||
let version = version.unwrap_or(self.manifest.version.clone());
|
||||
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(&self) -> PathBuf {
|
||||
self.paths.PackagesDir.join("VelopackTemp")
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Returns the current app's version.
|
||||
pub fn get_manifest_version(&self) -> Version {
|
||||
self.manifest.version.clone()
|
||||
}
|
||||
|
||||
/// Returns the current app's version as a string containing all parts.
|
||||
pub fn get_manifest_version_full_string(&self) -> String {
|
||||
self.manifest.version.to_string()
|
||||
}
|
||||
|
||||
/// Returns the current app's version as a string in short format (eg. '1.2.3'),
|
||||
/// not including any semver release groups etc.
|
||||
pub fn get_manifest_version_short_string(&self) -> String {
|
||||
let ver = &self.manifest.version;
|
||||
format!("{}.{}.{}", ver.major, ver.minor, ver.patch)
|
||||
}
|
||||
|
||||
/// Returns the current app package channel.
|
||||
pub fn get_manifest_channel(&self) -> String {
|
||||
self.manifest.channel.clone()
|
||||
}
|
||||
|
||||
/// Returns the current app's Id.
|
||||
pub fn get_manifest_id(&self) -> String {
|
||||
self.manifest.id.clone()
|
||||
}
|
||||
|
||||
/// Returns the current app's friendly / display name.
|
||||
pub fn get_manifest_title(&self) -> String {
|
||||
self.manifest.title.clone()
|
||||
}
|
||||
|
||||
/// Returns the current app authors / publishers string.
|
||||
pub fn get_manifest_authors(&self) -> String {
|
||||
self.manifest.authors.clone()
|
||||
}
|
||||
|
||||
/// Returns a flags enumeration of desired shortcut locations, or NONE if no shortcuts are desired.
|
||||
pub fn get_manifest_shortcut_locations(&self) -> ShortcutLocationFlags {
|
||||
if self.manifest.shortcut_locations.is_empty() {
|
||||
return ShortcutLocationFlags::NONE;
|
||||
}
|
||||
if self.manifest.shortcut_locations.to_ascii_lowercase() == "none" {
|
||||
return ShortcutLocationFlags::NONE;
|
||||
}
|
||||
ShortcutLocationFlags::from_string(&self.manifest.shortcut_locations)
|
||||
}
|
||||
|
||||
/// Returns the desired shortcut AMUID, or None if no AMUID has been provided.
|
||||
pub fn get_manifest_shortcut_amuid(&self) -> Option<String> {
|
||||
if self.manifest.shortcut_amuid.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(self.manifest.shortcut_amuid.clone())
|
||||
}
|
||||
|
||||
/// Returns a copy of the current VelopackLocator with the shortcut_locations
|
||||
/// field set to an empty string.
|
||||
pub fn clone_self_with_blank_shortcuts(&self) -> VelopackLocator
|
||||
{
|
||||
let mut new_manifest = self.manifest.clone();
|
||||
new_manifest.shortcut_locations = "".to_string();
|
||||
VelopackLocator {
|
||||
paths: self.paths.clone(),
|
||||
manifest: new_manifest,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a copy of the current VelopackLocator with the manifest field set to the given manifest.
|
||||
pub fn clone_self_with_new_manifest(&self, manifest: &Manifest) -> VelopackLocator
|
||||
{
|
||||
VelopackLocator {
|
||||
paths: self.paths.clone(),
|
||||
manifest: manifest.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the app is portable or installed.
|
||||
pub fn get_is_portable(&self) -> bool {
|
||||
self.paths.IsPortable
|
||||
}
|
||||
|
||||
fn path_as_string(path: &PathBuf) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a paths object containing default / ideal paths for a given root directory
|
||||
/// Generally, this should not be used except for installing the app for the first time.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn create_config_from_root_dir<P: AsRef<Path>>(root_dir: P) -> VelopackLocatorConfig
|
||||
{
|
||||
let root_dir = root_dir.as_ref();
|
||||
VelopackLocatorConfig {
|
||||
RootAppDir: root_dir.to_path_buf(),
|
||||
UpdateExePath: root_dir.join("Update.exe"),
|
||||
PackagesDir: root_dir.join("packages"),
|
||||
ManifestPath: root_dir.join("current").join("sq.version"),
|
||||
CurrentBinaryDir: root_dir.join("current"),
|
||||
IsPortable: root_dir.join(".portable").exists(),
|
||||
}
|
||||
}
|
||||
|
||||
fn config_to_locator(config: &VelopackLocatorConfig) -> Result<VelopackLocator, Error>
|
||||
{
|
||||
if !config.UpdateExePath.exists() {
|
||||
return Err(Error::MissingUpdateExe);
|
||||
}
|
||||
if !config.ManifestPath.exists() {
|
||||
return Err(Error::MissingNuspec);
|
||||
}
|
||||
|
||||
let manifest = read_current_manifest(&config.ManifestPath)?;
|
||||
Ok(VelopackLocator::new(config.clone(), manifest))
|
||||
}
|
||||
|
||||
/// LocationContext is an enumeration of possible contexts for locating the current app manifest.
|
||||
pub enum LocationContext
|
||||
{
|
||||
/// Should not really be used, will try a few other enumerations to locate the app manifest.
|
||||
Unknown,
|
||||
/// Locates the app manifest by assuming the current process is Update.exe.
|
||||
IAmUpdateExe,
|
||||
/// Locates the app manifest by assuming the current process is inside the application current/binary directory.
|
||||
FromCurrentExe,
|
||||
/// Locates the app manifest by assuming the app is installed in the specified root directory.
|
||||
FromSpecifiedRootDir(PathBuf),
|
||||
/// Locates the app manifest by assuming the specified path is inside the application current/binary directory.
|
||||
FromSpecifiedAppExecutable(PathBuf),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
|
||||
pub fn auto_locate_app_manifest(context: LocationContext) -> Result<VelopackLocator, Error> {
|
||||
match context {
|
||||
LocationContext::Unknown => {
|
||||
warn!("Unknown location context, trying to auto-locate from current exe location...");
|
||||
if let Ok(locator) = auto_locate_app_manifest(LocationContext::FromCurrentExe) {
|
||||
return Ok(locator);
|
||||
}
|
||||
if let Ok(locator) = auto_locate_app_manifest(LocationContext::IAmUpdateExe) {
|
||||
return Ok(locator);
|
||||
}
|
||||
}
|
||||
LocationContext::FromCurrentExe => {
|
||||
let current_exe = std::env::current_exe()?;
|
||||
return auto_locate_app_manifest(LocationContext::FromSpecifiedAppExecutable(current_exe));
|
||||
}
|
||||
LocationContext::FromSpecifiedRootDir(root_dir) => {
|
||||
let config = create_config_from_root_dir(&root_dir);
|
||||
let locator = config_to_locator(&config)?;
|
||||
return Ok(locator);
|
||||
}
|
||||
LocationContext::FromSpecifiedAppExecutable(exe_path) => {
|
||||
// check if Update.exe exists in parent dir, if it does, that's the root dir.
|
||||
if let Some(parent_dir) = exe_path.parent() {
|
||||
if parent_dir.join("Update.exe").exists() {
|
||||
info!("Found Update.exe in parent directory: {}", parent_dir.to_string_lossy());
|
||||
let config = create_config_from_root_dir(&parent_dir);
|
||||
let locator = config_to_locator(&config)?;
|
||||
return Ok(locator);
|
||||
}
|
||||
}
|
||||
|
||||
// see if we can find the current dir in the current path, if we're more nested than that.
|
||||
let path = exe_path.to_string_lossy();
|
||||
let idx = path.rfind("\\current\\");
|
||||
if let Some(i) = idx {
|
||||
let maybe_root = &path[..i];
|
||||
let maybe_root = PathBuf::from(maybe_root);
|
||||
if (maybe_root.join("Update.exe")).exists() {
|
||||
info!("Found Update.exe in parent directory: {}", maybe_root.to_string_lossy());
|
||||
return Ok(VelopackLocator {
|
||||
RootAppDir: maybe_root.clone(),
|
||||
UpdateExePath: maybe_root.join("Update.exe"),
|
||||
PackagesDir: maybe_root.join("packages"),
|
||||
ManifestPath: maybe_root.join("current").join("sq.version"),
|
||||
TempDir: maybe_root.join("packages").join("VelopackTemp"),
|
||||
});
|
||||
if maybe_root.join("Update.exe").exists() {
|
||||
info!("Found Update.exe by current path pattern search in directory: {}", maybe_root.to_string_lossy());
|
||||
let config = create_config_from_root_dir(&maybe_root);
|
||||
let locator = config_to_locator(&config)?;
|
||||
return Ok(locator);
|
||||
}
|
||||
}
|
||||
}
|
||||
LocationContext::IAmUpdateExe => {
|
||||
let exe_path = std::env::current_exe()?;
|
||||
if let Some(parent_dir) = exe_path.parent() {
|
||||
let config = create_config_from_root_dir(&parent_dir);
|
||||
let locator = config_to_locator(&config)?;
|
||||
return Ok(locator);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Err(Error::MissingUpdateExe)
|
||||
Err(Error::NotInstalled("Could not auto-locate app manifest".to_owned()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
|
||||
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error> {
|
||||
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocatorConfig, Error> {
|
||||
let path = exe_path.as_ref().to_path_buf();
|
||||
let path = path.to_string_lossy();
|
||||
let idx = path.rfind("/usr/bin/");
|
||||
@@ -129,7 +409,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
|
||||
}
|
||||
|
||||
let app = read_current_manifest(&metadata_path)?;
|
||||
Ok(VelopackLocator {
|
||||
Ok(VelopackLocatorConfig {
|
||||
RootAppDir: root_app_dir,
|
||||
UpdateExePath: update_exe_path,
|
||||
PackagesDir: PathBuf::from("/var/tmp/velopack").join(&app.id).join("packages"),
|
||||
@@ -140,7 +420,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
|
||||
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error> {
|
||||
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocatorConfig, Error> {
|
||||
let path = exe_path.as_ref().to_path_buf();
|
||||
let path = path.to_string_lossy();
|
||||
let idx = path.rfind(".app/");
|
||||
@@ -169,7 +449,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
|
||||
packages_dir.push(&app.id);
|
||||
packages_dir.push("packages");
|
||||
|
||||
Ok(VelopackLocator {
|
||||
Ok(VelopackLocatorConfig {
|
||||
RootAppDir: root_app_dir,
|
||||
UpdateExePath: update_exe_path,
|
||||
PackagesDir: packages_dir,
|
||||
@@ -181,7 +461,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
|
||||
fn read_current_manifest(nuspec_path: &PathBuf) -> Result<Manifest, Error> {
|
||||
if nuspec_path.exists() {
|
||||
if let Ok(nuspec) = util::retry_io(|| std::fs::read_to_string(&nuspec_path)) {
|
||||
return Ok(manifest::read_manifest_from_string(&nuspec)?);
|
||||
return Ok(bundle::read_manifest_from_string(&nuspec)?);
|
||||
}
|
||||
}
|
||||
Err(Error::MissingNuspec)
|
||||
|
||||
@@ -14,11 +14,11 @@ use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
locator::{self, VelopackLocator},
|
||||
manifest::Manifest,
|
||||
locator::{self, VelopackLocatorConfig},
|
||||
sources::UpdateSource,
|
||||
Error,
|
||||
};
|
||||
use crate::locator::{auto_locate_app_manifest, LocationContext, VelopackLocator};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
@@ -106,11 +106,9 @@ pub struct UpdateOptions {
|
||||
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
|
||||
#[derive(Clone)]
|
||||
pub struct UpdateManager {
|
||||
allow_version_downgrade: bool,
|
||||
explicit_channel: Option<String>,
|
||||
options: UpdateOptions,
|
||||
source: Box<dyn UpdateSource>,
|
||||
locator: VelopackLocator,
|
||||
manifest: Manifest,
|
||||
}
|
||||
|
||||
/// Represents the result of a call to check for updates.
|
||||
@@ -136,40 +134,40 @@ impl UpdateManager {
|
||||
pub fn new<T: UpdateSource>(
|
||||
source: T,
|
||||
options: Option<UpdateOptions>,
|
||||
locator: Option<VelopackLocator>,
|
||||
locator: Option<VelopackLocatorConfig>,
|
||||
) -> Result<UpdateManager, Error> {
|
||||
let locator = if let Some(loc) = locator { loc } else {
|
||||
let my_exe = std::env::current_exe()?;
|
||||
locator::auto_locate(my_exe)?
|
||||
let locator = if let Some(config) = locator {
|
||||
let manifest = config.load_manifest()?;
|
||||
VelopackLocator::new(config.clone(), manifest)
|
||||
} else {
|
||||
auto_locate_app_manifest(LocationContext::FromCurrentExe)?
|
||||
};
|
||||
let manifest = locator.load_manifest()?;
|
||||
Ok(UpdateManager {
|
||||
allow_version_downgrade: options.as_ref().map(|f| f.AllowVersionDowngrade).unwrap_or(false),
|
||||
explicit_channel: options.as_ref().map(|f| f.ExplicitChannel.clone()).unwrap_or(None),
|
||||
options: options.unwrap_or_default(),
|
||||
source: source.clone_boxed(),
|
||||
locator,
|
||||
manifest,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_practical_channel(&self) -> String {
|
||||
let channel = self.explicit_channel.as_deref();
|
||||
let mut channel = channel.unwrap_or(&self.manifest.channel).to_string();
|
||||
let options_channel = self.options.ExplicitChannel.as_deref();
|
||||
let app_channel = self.locator.get_manifest_channel();
|
||||
let mut channel = options_channel.unwrap_or(&app_channel).to_string();
|
||||
if channel.is_empty() {
|
||||
channel = locator::default_channel_name();
|
||||
}
|
||||
channel
|
||||
}
|
||||
|
||||
/// The currently installed app version when you created your release.
|
||||
/// The currently installed app version.
|
||||
pub fn current_version(&self) -> Result<String, Error> {
|
||||
Ok(self.manifest.version.to_string())
|
||||
Ok(self.locator.get_manifest_version_full_string())
|
||||
}
|
||||
|
||||
/// Get a list of available remote releases from the package source.
|
||||
pub fn get_release_feed(&self) -> Result<VelopackAssetFeed, Error> {
|
||||
let channel = self.get_practical_channel();
|
||||
self.source.get_release_feed(&channel, &self.manifest)
|
||||
self.source.get_release_feed(&channel, &self.locator.get_manifest())
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
@@ -183,13 +181,14 @@ impl UpdateManager {
|
||||
/// Checks for updates, returning None if there are none available. If there are updates available, this method will return an
|
||||
/// UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available.
|
||||
pub fn check_for_updates(&self) -> Result<UpdateCheck, Error> {
|
||||
let allow_downgrade = self.allow_version_downgrade;
|
||||
let app = &self.manifest;
|
||||
let allow_downgrade = self.options.AllowVersionDowngrade;
|
||||
let app_channel = self.locator.get_manifest_channel();
|
||||
let app_version = self.locator.get_manifest_version();
|
||||
let feed = self.get_release_feed()?;
|
||||
let assets = feed.Assets;
|
||||
|
||||
let practical_channel = self.get_practical_channel();
|
||||
let is_non_default_channel = practical_channel != app.channel;
|
||||
let is_non_default_channel = practical_channel != app_channel;
|
||||
|
||||
if assets.is_empty() {
|
||||
return Ok(UpdateCheck::RemoteIsEmpty);
|
||||
@@ -218,16 +217,16 @@ impl UpdateManager {
|
||||
|
||||
debug!("Latest remote release: {} ({}).", remote_asset.FileName, remote_version.to_string());
|
||||
|
||||
if remote_version > app.version {
|
||||
info!("Found newer remote release available ({} -> {}).", app.version, remote_version);
|
||||
if remote_version > app_version {
|
||||
info!("Found newer remote release available ({} -> {}).", app_version, remote_version);
|
||||
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: false }))
|
||||
} else if remote_version < app.version && allow_downgrade {
|
||||
info!("Found older remote release available and downgrade is enabled ({} -> {}).", app.version, remote_version);
|
||||
} else if remote_version < app_version && allow_downgrade {
|
||||
info!("Found older remote release available and downgrade is enabled ({} -> {}).", app_version, remote_version);
|
||||
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true }))
|
||||
} else if remote_version == app.version && allow_downgrade && is_non_default_channel {
|
||||
} else if remote_version == app_version && allow_downgrade && is_non_default_channel {
|
||||
info!(
|
||||
"Latest remote release is the same version of a different channel, and downgrade is enabled ({} -> {}).",
|
||||
app.version, remote_version
|
||||
app_version, remote_version
|
||||
);
|
||||
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true }))
|
||||
} else {
|
||||
@@ -252,7 +251,8 @@ impl UpdateManager {
|
||||
/// packages, this method will fall back to downloading the full version of the update.
|
||||
pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error> {
|
||||
let name = &update.TargetFullRelease.FileName;
|
||||
let packages_dir = &self.locator.PackagesDir;
|
||||
let packages_dir = &self.locator.get_packages_dir();
|
||||
|
||||
fs::create_dir_all(packages_dir)?;
|
||||
let target_file = packages_dir.join(name);
|
||||
|
||||
@@ -286,7 +286,8 @@ impl UpdateManager {
|
||||
match crate::bundle::load_bundle_from_file(&target_file) {
|
||||
Ok(bundle) => {
|
||||
info!("Bundle loaded successfully.");
|
||||
if let Err(e) = bundle.extract_zip_predicate_to_path(|f| f.ends_with("Squirrel.exe"), &self.locator.UpdateExePath) {
|
||||
let update_exe_path = self.locator.get_update_path();
|
||||
if let Err(e) = bundle.extract_zip_predicate_to_path(|f| f.ends_with("Squirrel.exe"), update_exe_path) {
|
||||
error!("Error extracting Update.exe from bundle: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -377,7 +378,7 @@ impl UpdateManager {
|
||||
C: IntoIterator<Item=S>,
|
||||
{
|
||||
let to_apply = to_apply.as_ref();
|
||||
let pkg_path = self.locator.PackagesDir.join(&to_apply.FileName);
|
||||
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();
|
||||
@@ -403,9 +404,9 @@ impl UpdateManager {
|
||||
}
|
||||
}
|
||||
|
||||
let mut p = Process::new(&self.locator.UpdateExePath);
|
||||
let mut p = Process::new(&self.locator.get_update_path());
|
||||
p.args(&args);
|
||||
p.current_dir(&self.locator.RootAppDir);
|
||||
p.current_dir(&self.locator.get_root_dir());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -413,7 +414,7 @@ impl UpdateManager {
|
||||
p.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
info!("About to run Update.exe: {} {:?}", self.locator.UpdateExePath.to_string_lossy(), args);
|
||||
info!("About to run Update.exe: {} {:?}", self.locator.get_update_path_as_string(), args);
|
||||
p.spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use semver::Version;
|
||||
use xml::reader::{EventReader, XmlEvent};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, derivative::Derivative, Clone)]
|
||||
#[derivative(Default)]
|
||||
pub struct Manifest {
|
||||
pub id: String,
|
||||
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
|
||||
pub version: Version,
|
||||
pub title: String,
|
||||
pub authors: String,
|
||||
pub description: String,
|
||||
pub machine_architecture: String,
|
||||
pub runtime_dependencies: String,
|
||||
pub main_exe: String,
|
||||
pub os: String,
|
||||
pub os_min_version: String,
|
||||
pub channel: String,
|
||||
}
|
||||
|
||||
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest, Error> {
|
||||
let mut obj: Manifest = Default::default();
|
||||
let cursor = Cursor::new(xml);
|
||||
let parser = EventReader::new(cursor);
|
||||
let mut vec: Vec<String> = Vec::new();
|
||||
for e in parser {
|
||||
match e {
|
||||
Ok(XmlEvent::StartElement { name, .. }) => {
|
||||
vec.push(name.local_name);
|
||||
}
|
||||
Ok(XmlEvent::Characters(text)) => {
|
||||
if vec.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let el_name = vec.last().unwrap();
|
||||
if el_name == "id" {
|
||||
obj.id = text;
|
||||
} else if el_name == "version" {
|
||||
obj.version = Version::parse(&text)?;
|
||||
} else if el_name == "title" {
|
||||
obj.title = text;
|
||||
} else if el_name == "authors" {
|
||||
obj.authors = text;
|
||||
} else if el_name == "description" {
|
||||
obj.description = text;
|
||||
} else if el_name == "machineArchitecture" {
|
||||
obj.machine_architecture = text;
|
||||
} else if el_name == "runtimeDependencies" {
|
||||
obj.runtime_dependencies = text;
|
||||
} else if el_name == "mainExe" {
|
||||
obj.main_exe = text;
|
||||
} else if el_name == "os" {
|
||||
obj.os = text;
|
||||
} else if el_name == "osMinVersion" {
|
||||
obj.os_min_version = text;
|
||||
} else if el_name == "channel" {
|
||||
obj.channel = text;
|
||||
}
|
||||
}
|
||||
Ok(XmlEvent::EndElement { .. }) => {
|
||||
vec.pop();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error: {e}");
|
||||
break;
|
||||
}
|
||||
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.id.is_empty() {
|
||||
return Err(Error::MissingNuspecProperty("id".to_owned()));
|
||||
}
|
||||
|
||||
if obj.version == Version::new(0, 0, 0) {
|
||||
return Err(Error::MissingNuspecProperty("version".to_owned()));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if obj.main_exe.is_empty() {
|
||||
return Err(Error::MissingNuspecProperty("mainExe".to_owned()));
|
||||
}
|
||||
|
||||
if obj.title.is_empty() {
|
||||
obj.title = obj.id.clone();
|
||||
}
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use crate::*;
|
||||
pub trait UpdateSource: Send + Sync {
|
||||
/// Retrieve the list of available remote releases from the package source. These releases
|
||||
/// can subsequently be downloaded with download_release_entry.
|
||||
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error>;
|
||||
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> 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>;
|
||||
/// Clone the source to create a new lifetime.
|
||||
@@ -51,7 +51,7 @@ impl AutoSource {
|
||||
}
|
||||
|
||||
impl UpdateSource for AutoSource {
|
||||
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
self.source.get_release_feed(channel, app)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl HttpSource {
|
||||
}
|
||||
|
||||
impl UpdateSource for HttpSource {
|
||||
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
let releases_name = format!("releases.{}.json", channel);
|
||||
|
||||
let path = self.url.trim_end_matches('/').to_owned() + "/";
|
||||
@@ -129,7 +129,7 @@ impl FileSource {
|
||||
}
|
||||
|
||||
impl UpdateSource for FileSource {
|
||||
fn get_release_feed(&self, channel: &str, _: &manifest::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
fn get_release_feed(&self, channel: &str, _: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
let releases_name = format!("releases.{}.json", channel);
|
||||
let releases_path = self.path.join(&releases_name);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user