Implement update.exe apply

This commit is contained in:
Caelan Sayler
2023-12-23 19:41:33 +00:00
parent 19fc1be949
commit 565fd7eddb
5 changed files with 188 additions and 16 deletions

View File

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

View File

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

View File

@@ -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, &current_path, Some(Duration::from_secs(30))) {
let setup_name = format!("{} Setup {}", app.title, app.version);
error!("Process install hook failed: {}", e);

View File

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

View File

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