Consolidate duplicate rust code (bins & lib-rust)

This commit is contained in:
Caelan
2024-09-26 11:31:50 -06:00
parent 9086b0d09f
commit 6ce3733976
42 changed files with 1317 additions and 1437 deletions

16
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"

View File

@@ -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.");
}

View File

@@ -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, &current_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)
}

View File

@@ -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(())

View File

@@ -1,9 +1,6 @@
mod apply;
pub use apply::*;
mod patch;
pub use patch::*;
mod start;
pub use start::*;

View File

@@ -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;
}

View File

@@ -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)?;

View File

@@ -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(&current).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(&current);
@@ -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(&current_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, &current_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)
}

View File

@@ -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");

View File

@@ -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;

View File

@@ -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));

View File

@@ -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.");
}

View File

@@ -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(&current_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(&current_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());
}

View File

@@ -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")) {

View File

@@ -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));
}

View File

@@ -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();
}

View File

@@ -1,5 +1,5 @@
pub mod bundle;
pub mod download;
// pub mod bundle;
// pub mod download;
pub mod macho;
pub mod runtime_arch;

View File

@@ -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);
}

View File

@@ -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")]

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View 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(())
}

View File

@@ -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";

View File

@@ -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() {

View File

@@ -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());

View File

@@ -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);

View File

@@ -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"] }

View File

@@ -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();
}

View File

@@ -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)
}

View 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, };

View File

@@ -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, };

View File

@@ -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 }

View File

@@ -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))?
}
}

View File

@@ -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(&current_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(&current_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());
}

View File

@@ -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()?;

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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(())
}

View File

@@ -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)
}

View File

@@ -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);