mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Implement update.exe apply
This commit is contained in:
@@ -173,6 +173,42 @@ impl BundleInfo<'_> {
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
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 current directory...", num_files);
|
||||
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
|
||||
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
|
||||
let updater_idx = self.find_zip_file(|name| name.ends_with("Squirrel.exe"));
|
||||
|
||||
for (i, key) in files.iter().enumerate() {
|
||||
if Some(i) == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") {
|
||||
info!(" {} Skipped '{}'", i, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path_in_zip = re.replace(key, "").to_string();
|
||||
let file_path_on_disk = Path::new(¤t_path).join(&file_path_in_zip);
|
||||
|
||||
if 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);
|
||||
info!(" {} Skipped Stub (obsolete) '{}'", i, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
let final_path = file_path_on_disk.to_str().unwrap().replace("/", "\\");
|
||||
info!(" {} Extracting '{}' to '{}'", i, key, final_path);
|
||||
|
||||
self.extract_zip_idx_to_path(i, &final_path)?;
|
||||
progress(((i as f32 / num_files as f32) * 100.0) as i16);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_manifest(&self) -> Result<Manifest> {
|
||||
let nuspec_idx = self
|
||||
.find_zip_file(|name| name.ends_with(".nuspec"))
|
||||
@@ -211,10 +247,12 @@ impl BundleInfo<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, derivative::Derivative, Clone)]
|
||||
#[derivative(Default)]
|
||||
pub struct Manifest {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
|
||||
pub version: Version,
|
||||
pub title: String,
|
||||
pub authors: String,
|
||||
pub description: String,
|
||||
@@ -254,7 +292,7 @@ impl Manifest {
|
||||
let updater_path = self.get_update_path(root_path);
|
||||
|
||||
let folder_size = fs_extra::dir::get_size(&root_path).unwrap();
|
||||
let sver = semver::Version::parse(&self.version)?;
|
||||
let sver = &self.version;
|
||||
let sver_str = format!("{}.{}.{}", sver.major, sver.minor, sver.patch);
|
||||
|
||||
let now = DateTime::now();
|
||||
@@ -305,7 +343,7 @@ pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
|
||||
if el_name == "id" {
|
||||
obj.id = text;
|
||||
} else if el_name == "version" {
|
||||
obj.version = text;
|
||||
obj.version = Version::parse(&text)?;
|
||||
} else if el_name == "title" {
|
||||
obj.title = text;
|
||||
} else if el_name == "authors" {
|
||||
@@ -344,7 +382,7 @@ pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
|
||||
bail!("Unsupported 'os' in package manifest ({}). Please contact the application author.", obj.os);
|
||||
}
|
||||
|
||||
if obj.version.is_empty() {
|
||||
if obj.version == Version::new(0, 0, 0) {
|
||||
bail!("Missing 'version' in package manifest. Please contact the application author.");
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +253,8 @@ pub fn test_os_returns_true_for_everything_on_windows_11_and_below() {
|
||||
assert!(!is_os_version_or_greater("12").unwrap());
|
||||
}
|
||||
|
||||
pub fn get_processes_running_in_directory(dir: &PathBuf) -> Result<HashMap<u32, PathBuf>> {
|
||||
pub fn get_processes_running_in_directory<P: AsRef<Path>>(dir: P) -> Result<HashMap<u32, PathBuf>> {
|
||||
let dir = dir.as_ref();
|
||||
let mut oup = HashMap::new();
|
||||
let mut hpl = w::HPROCESSLIST::CreateToolhelp32Snapshot(co::TH32CS::SNAPPROCESS, None)?;
|
||||
for proc_entry in hpl.iter_processes() {
|
||||
@@ -287,7 +288,8 @@ pub fn kill_pid(pid: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kill_processes_in_directory(dir: &PathBuf) -> Result<()> {
|
||||
pub fn kill_processes_in_directory<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
let dir = dir.as_ref();
|
||||
info!("Checking for running processes in: {}", dir.display());
|
||||
let processes = get_processes_running_in_directory(dir)?;
|
||||
let my_pid = std::process::id();
|
||||
|
||||
@@ -330,8 +330,9 @@ fn install_app(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::mp
|
||||
warn!("Failed to create start menu shortcut: {}", e);
|
||||
}
|
||||
|
||||
info!("Starting process install hook: \"{}\" --squirrel-install {}", main_exe_path, &app.version);
|
||||
let args = vec!["--squirrel-install", &app.version];
|
||||
let ver_string = app.version.to_string();
|
||||
info!("Starting process install hook: \"{}\" --squirrel-install {}", &main_exe_path, &ver_string);
|
||||
let args = vec!["--squirrel-install", &ver_string];
|
||||
if let Err(e) = platform::run_process_no_console_and_wait(&main_exe_path, args, ¤t_path, Some(Duration::from_secs(30))) {
|
||||
let setup_name = format!("{} Setup {}", app.title, app.version);
|
||||
error!("Process install hook failed: {}", e);
|
||||
|
||||
@@ -14,11 +14,14 @@ extern crate simplelog;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use bundle::Manifest;
|
||||
use clap::{arg, value_parser, ArgMatches, Command};
|
||||
use glob::glob;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
use crate::bundle::BundleInfo;
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn root_command() -> Command {
|
||||
Command::new("Update")
|
||||
@@ -121,6 +124,11 @@ fn start(matches: &ArgMatches) -> Result<()> {
|
||||
info!(" Exe Name: {:?}", exe_name);
|
||||
info!(" Exe Args: {:?}", exe_args);
|
||||
|
||||
_start(wait_for_parent, exe_name, exe_args, legacy_args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _start(wait_for_parent: bool, exe_name: Option<&String>, exe_args: Option<Vec<&str>>, legacy_args: Option<&String>) -> Result<()> {
|
||||
if legacy_args.is_some() {
|
||||
info!(" Legacy Args: {:?}", legacy_args);
|
||||
warn!("Legacy args format is deprecated and will be removed in a future release. Please update your application to use the new format.");
|
||||
@@ -163,11 +171,132 @@ fn start(matches: &ArgMatches) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply(_matches: &ArgMatches) -> Result<()> {
|
||||
info!("Command: Apply");
|
||||
let root_path = get_my_root_dir()?;
|
||||
fn apply<'a>(matches: &ArgMatches) -> Result<()> {
|
||||
let restart = matches.get_flag("restart");
|
||||
let wait_for_parent = matches.get_flag("wait");
|
||||
let package = matches.get_one::<PathBuf>("package");
|
||||
let exe_name = matches.get_one::<String>("EXE_NAME");
|
||||
let exe_args: Option<Vec<&str>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect());
|
||||
|
||||
todo!();
|
||||
info!("Command: Apply");
|
||||
info!(" Restart: {:?}", restart);
|
||||
info!(" Wait: {:?}", wait_for_parent);
|
||||
info!(" Package: {:?}", package);
|
||||
info!(" Exe Name: {:?}", exe_name);
|
||||
info!(" Exe Args: {:?}", exe_args);
|
||||
|
||||
if let Err(e) = apply_package(package) {
|
||||
error!("Error applying package: {}", e);
|
||||
if !restart {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
if restart {
|
||||
_start(wait_for_parent, exe_name, exe_args, None)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_package<'a>(package: Option<&PathBuf>) -> Result<()> {
|
||||
let mut package_manifest: Option<Manifest> = None;
|
||||
let mut package_bundle: Option<BundleInfo<'a>> = None;
|
||||
|
||||
let (root_path, app) = init_root()?;
|
||||
|
||||
if let Some(pkg) = package {
|
||||
info!("Loading package from argument '{}'.", pkg.to_string_lossy());
|
||||
let bun = bundle::load_bundle_from_file(&pkg)?;
|
||||
package_manifest = Some(bun.read_manifest()?);
|
||||
package_bundle = Some(bun);
|
||||
} else {
|
||||
info!("No package specified, searching for latest.");
|
||||
let packages_dir = app.get_packages_path(&root_path);
|
||||
if let Ok(paths) = glob(format!("{}/*.nupkg", packages_dir).as_str()) {
|
||||
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(mani) = bun.read_manifest() {
|
||||
if package_manifest.is_none() || mani.version > package_manifest.clone().unwrap().version {
|
||||
info!("Found {}: '{}'", mani.version, path.to_string_lossy());
|
||||
package_manifest = Some(mani);
|
||||
package_bundle = Some(bun);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if package_manifest.is_none() || package_bundle.is_none() {
|
||||
bail!("Unable to find/load suitable package.");
|
||||
}
|
||||
|
||||
let found_version = package_manifest.unwrap().version;
|
||||
if found_version <= app.version {
|
||||
bail!("Latest package found is {}, which is not newer than current version {}.", found_version, app.version);
|
||||
}
|
||||
|
||||
let current_dir = app.get_current_path(&root_path);
|
||||
replace_dir_with_rollback(current_dir.clone(), || {
|
||||
if let Some(bundle) = package_bundle.take() {
|
||||
bundle.extract_lib_contents_to_path(¤t_dir, |_| {})
|
||||
} else {
|
||||
bail!("No bundle could be loaded.");
|
||||
}
|
||||
})?;
|
||||
|
||||
info!("Package applied successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn replace_dir_with_rollback<F, T, P: AsRef<Path>>(path: P, op: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce() -> Result<T>,
|
||||
{
|
||||
let path = path.as_ref().to_string_lossy().to_string();
|
||||
let mut path_renamed = String::new();
|
||||
|
||||
if !util::is_dir_empty(&path) {
|
||||
path_renamed = format!("{}_{}", path, util::random_string(8));
|
||||
info!("Renaming directory '{}' to '{}' to allow rollback...", path, path_renamed);
|
||||
|
||||
platform::kill_processes_in_directory(&path)
|
||||
.map_err(|z| anyhow!("Failed to stop application ({}), please close the application and try running the installer again.", z))?;
|
||||
|
||||
util::retry_io(|| fs::rename(&path, &path_renamed)).map_err(|z| anyhow!("Failed to rename directory '{}' to '{}' ({}).", path, path_renamed, z))?;
|
||||
}
|
||||
|
||||
remove_dir_all::ensure_empty_dir(&path).map_err(|z| anyhow!("Failed to create clean directory '{}' ({}).", path, z))?;
|
||||
|
||||
if let Err(e) = op() {
|
||||
// install failed, rollback if possible
|
||||
warn!("Rolling back installation... (error was: {:?})", e);
|
||||
if let Err(ex) = platform::kill_processes_in_directory(&path) {
|
||||
warn!("Failed to stop application ({}).", ex);
|
||||
}
|
||||
if !path_renamed.is_empty() {
|
||||
if let Err(ex) = util::retry_io(|| fs::remove_dir_all(&path)) {
|
||||
error!("Failed to remove directory '{}' ({}).", path, ex);
|
||||
}
|
||||
if let Err(ex) = util::retry_io(|| fs::rename(&path_renamed, &path)) {
|
||||
error!("Failed to rename directory '{}' to '{}' ({}).", path_renamed, path, ex);
|
||||
}
|
||||
}
|
||||
return Err(e);
|
||||
} else {
|
||||
// install successful, remove rollback directory if exists
|
||||
if !path_renamed.is_empty() {
|
||||
debug!("Removing rollback directory '{}'.", path_renamed);
|
||||
if let Err(ex) = util::retry_io(|| fs::remove_dir_all(&path_renamed)) {
|
||||
warn!("Failed to remove directory '{}' ({}).", path_renamed, ex);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
fn init_root() -> Result<(PathBuf, Manifest)> {
|
||||
@@ -194,7 +323,8 @@ fn uninstall(_matches: &ArgMatches, log_file: &PathBuf) -> Result<()> {
|
||||
|
||||
// run uninstall hook
|
||||
info!("Running uninstall hook...");
|
||||
let args = vec!["--squirrel-install", &app.version];
|
||||
let ver_string = app.version.to_string();
|
||||
let args = vec!["--squirrel-install", &ver_string];
|
||||
if let Err(e) = platform::run_process_no_console_and_wait(&main_exe_path, args, ¤t_path, Some(Duration::from_secs(30))) {
|
||||
error!("Uninstall hook failed: {}", e);
|
||||
// for now, i'm ignoring hook failures as we stil should be able to clean up all files
|
||||
|
||||
@@ -5,7 +5,7 @@ use regex::Regex;
|
||||
use simplelog::*;
|
||||
use std::{
|
||||
io::{self},
|
||||
path::PathBuf,
|
||||
path::{PathBuf, Path},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -53,7 +53,8 @@ pub fn random_string(len: usize) -> String {
|
||||
Alphanumeric.sample_string(&mut rand::thread_rng(), len)
|
||||
}
|
||||
|
||||
pub fn is_dir_empty(path: &PathBuf) -> bool {
|
||||
pub fn is_dir_empty<P: AsRef<Path>>(path: P) -> bool {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user