Add re/start command for linux & mac (#158)

* Add start command for linux/mac
* Add start command to UpdateExe
This commit is contained in:
Caelan
2024-07-04 20:41:38 +01:00
committed by GitHub
parent fc90f75147
commit 1083d569e6
5 changed files with 212 additions and 151 deletions

View File

@@ -4,6 +4,9 @@ pub use apply::*;
mod patch;
pub use patch::*;
mod start;
pub use start::*;
#[cfg(target_os = "linux")]
mod apply_linux_impl;
#[cfg(target_os = "macos")]
@@ -12,9 +15,7 @@ mod apply_osx_impl;
mod apply_windows_impl;
#[cfg(target_os = "windows")]
mod start;
#[cfg(target_os = "windows")]
pub use start::*;
mod start_windows_impl;
#[cfg(target_os = "windows")]
mod install;

View File

@@ -1,113 +1,33 @@
use crate::{
dialogs,
shared::{self, bundle, OperationWait},
windows as win,
};
use anyhow::{anyhow, bail, Result};
use std::os::windows::process::CommandExt;
use std::{
fs,
path::{Path, PathBuf},
process::Command as Process,
};
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use std::path::PathBuf;
pub fn start(wait: OperationWait, exe_name: Option<&String>, exe_args: Option<Vec<&str>>, legacy_args: Option<&String>) -> Result<()> {
use anyhow::Result;
use crate::bundle::Manifest;
use crate::shared::{self, OperationWait};
#[allow(unused_variables, unused_imports)]
pub fn start(
root_dir: &PathBuf,
app: &Manifest,
wait: OperationWait,
exe_name: Option<&String>,
exe_args: Option<Vec<&str>>,
legacy_args: Option<&String>,
) -> Result<()> {
use anyhow::bail;
#[cfg(target_os = "windows")]
if legacy_args.is_some() && exe_args.is_some() {
bail!("Cannot use both legacy args and new args format.");
}
shared::operation_wait(wait);
let (root_dir, app) = shared::detect_current_manifest()?;
#[cfg(target_os = "windows")]
super::start_windows_impl::start_impl(&root_dir, &app, exe_name, exe_args, legacy_args)?;
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)?;
if let Err(e) = try_legacy_migration(&root_dir, &app) {
warn!("Failed to migrate legacy app ({}).", e);
dialogs::show_error(
&app.title,
Some("Unable to start app"),
"This app installation has been corrupted and cannot be started. Please reinstall the app.",
);
return 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);
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()
};
if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy());
}
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current);
let mut cmd = Process::new(&exe_to_execute);
cmd.current_dir(&current);
if let Some(args) = exe_args {
cmd.args(args);
} else if let Some(args) = legacy_args {
cmd.raw_arg(args);
}
let cmd = cmd.spawn()?;
let _ = unsafe { AllowSetForegroundWindow(cmd.id()) };
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
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);
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)? {
fs::rename(&latest_app_dir, &current_dir)?;
}
}
info!("Applying latest full package...");
let buf = Path::new(&package.file_path).to_path_buf();
super::apply(&root_dir, &app, false, OperationWait::NoWait, Some(&buf), None, false)?;
info!("Removing old app-* folders...");
shared::delete_app_prefixed_folders(&root_dir)?;
let _ = remove_dir_all::remove_dir_all(root_dir.join("staging"));
info!("Removing old shortcuts...");
if let Err(e) = win::remove_all_shortcuts_for_root_dir(&root_dir) {
warn!("Failed to remove shortcuts ({}).", e);
}
info!("Creating new default shortcuts...");
let _ = win::create_default_lnks(&root_dir, &app);
#[cfg(not(target_os = "windows"))]
shared::start_package(&app, &root_dir, exe_args, None)?;
Ok(())
}

View File

@@ -0,0 +1,106 @@
use crate::bundle::Manifest;
use crate::{
dialogs,
shared::{self, bundle, OperationWait},
windows as win,
};
use anyhow::{anyhow, bail, Result};
use std::os::windows::process::CommandExt;
use std::{
fs,
path::{Path, PathBuf},
process::Command as Process,
};
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
pub fn start_impl(root_dir: &PathBuf, app: &Manifest, exe_name: Option<&String>, exe_args: Option<Vec<&str>>, legacy_args: Option<&String>) -> Result<()> {
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)?;
if let Err(e) = try_legacy_migration(root_dir, app) {
warn!("Failed to migrate legacy app ({}).", e);
dialogs::show_error(
&app.title,
Some("Unable to start app"),
"This app installation has been corrupted and cannot be started. Please reinstall the app.",
);
return 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);
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()
};
if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy());
}
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current);
let mut cmd = Process::new(&exe_to_execute);
cmd.current_dir(&current);
if let Some(args) = exe_args {
cmd.args(args);
} else if let Some(args) = legacy_args {
cmd.raw_arg(args);
}
let cmd = cmd.spawn()?;
let _ = unsafe { AllowSetForegroundWindow(cmd.id()) };
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
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);
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)? {
fs::rename(latest_app_dir, &current_dir)?;
}
}
info!("Applying latest full package...");
let buf = Path::new(&package.file_path).to_path_buf();
super::apply(root_dir, app, false, OperationWait::NoWait, Some(&buf), None, false)?;
info!("Removing old app-* folders...");
shared::delete_app_prefixed_folders(root_dir)?;
let _ = remove_dir_all::remove_dir_all(root_dir.join("staging"));
info!("Removing old shortcuts...");
if let Err(e) = win::remove_all_shortcuts_for_root_dir(root_dir) {
warn!("Failed to remove shortcuts ({}).", e);
}
info!("Creating new default shortcuts...");
let _ = win::create_default_lnks(root_dir, app);
Ok(())
}

View File

@@ -17,11 +17,20 @@ fn root_command() -> Command {
.subcommand(Command::new("apply")
.about("Applies a staged / prepared update, installing prerequisite runtimes if necessary")
.arg(arg!(--norestart "Do not restart the application after the update"))
.arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update"))
.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..))
)
.subcommand(Command::new("start")
.about("Starts the currently installed version of the application")
.arg(arg!(-a --args <ARGS> "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1))
.arg(arg!(-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..))
.long_flag_aliases(vec!["processStart", "processStartAndWait"])
)
.subcommand(Command::new("patch")
.about("Applies a Zstd patch file")
.arg(arg!(--old <FILE> "Base / old file to apply the patch to").required(true).value_parser(value_parser!(PathBuf)))
@@ -41,17 +50,6 @@ fn root_command() -> Command {
.disable_help_subcommand(true)
.flatten_help(true);
#[cfg(target_os = "windows")]
let cmd = cmd.subcommand(Command::new("start")
.about("Starts the currently installed version of the application")
.arg(arg!(-a --args <ARGS> "Legacy args format").aliases(vec!["processStartArgs", "process-start-args"]).hide(true).allow_hyphen_values(true).num_args(1))
.arg(arg!(-w --wait "Wait for the parent process to terminate before starting the application"))
.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..))
.long_flag_aliases(vec!["processStart", "processStartAndWait"])
);
#[cfg(target_os = "windows")]
let cmd = cmd.subcommand(Command::new("uninstall")
.about("Remove all app shortcuts, files, and registry entries.")
@@ -102,10 +100,10 @@ fn get_flag_or_false(matches: &ArgMatches, id: &str) -> bool {
fn get_op_wait(matches: &ArgMatches) -> shared::OperationWait {
let wait_for_parent = get_flag_or_false(&matches, "wait");
let wait_pid = matches.try_get_one::<u32>("waitPid").unwrap_or(None).map(|v| v.to_owned());
if wait_for_parent {
shared::OperationWait::WaitParent
} else if wait_pid.is_some() {
if wait_pid.is_some() {
shared::OperationWait::WaitPid(wait_pid.unwrap())
} else if wait_for_parent {
shared::OperationWait::WaitParent
} else {
shared::OperationWait::NoWait
}
@@ -151,11 +149,10 @@ fn main() -> Result<()> {
let result = match subcommand {
#[cfg(target_os = "windows")]
"uninstall" => uninstall(subcommand_matches).map_err(|e| anyhow!("Uninstall error: {}", e)),
#[cfg(target_os = "windows")]
"start" => start(subcommand_matches).map_err(|e| anyhow!("Start error: {}", e)),
"apply" => apply(subcommand_matches).map_err(|e| anyhow!("Apply error: {}", e)),
"patch" => patch(subcommand_matches).map_err(|e| anyhow!("Patch error: {}", e)),
_ => bail!("Unknown subcommand. Try `--help` for more information."),
_ => bail!("Unknown subcommand '{subcommand}'. Try `--help` for more information."),
};
if let Err(e) = result {
@@ -197,7 +194,6 @@ fn apply(matches: &ArgMatches) -> Result<()> {
commands::apply(&root_path, &app, restart, wait, package, exe_args, true)
}
#[cfg(target_os = "windows")]
fn start(matches: &ArgMatches) -> Result<()> {
let legacy_args = matches.get_one::<String>("args");
let exe_name = matches.get_one::<String>("EXE_NAME");
@@ -213,9 +209,10 @@ 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 (root_path, app) = shared::detect_current_manifest()?;
#[cfg(target_os = "windows")]
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?;
commands::start(wait, exe_name, exe_args, legacy_args)
commands::start(&root_path, &app, wait, exe_name, exe_args, legacy_args)
}
#[cfg(target_os = "windows")]

View File

@@ -21,6 +21,66 @@ namespace Velopack
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AllowSetForegroundWindow(int dwProcessId);
private static Process StartUpdateExe(ILogger logger, IVelopackLocator locator, IEnumerable<string> args)
{
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = locator.UpdateExePath!,
WorkingDirectory = Path.GetDirectoryName(locator.UpdateExePath)!,
};
psi.AppendArgumentListSafe(args, out var debugArgs);
logger.Debug($"Running: {psi.FileName} {debugArgs}");
var p = Process.Start(psi);
if (p == null) {
throw new Exception("Failed to launch Update.exe process.");
}
if (VelopackRuntimeInfo.IsWindows) {
try {
// this is an attempt to work around a bug where the restarted app fails to come to foreground.
AllowSetForegroundWindow(p.Id);
} catch (Exception ex) {
logger.LogWarning(ex, "Failed to allow Update.exe to set foreground window.");
}
}
logger.Info("Update.exe executed successfully.");
return p;
}
/// <summary>
/// Runs Update.exe in the current working directory with the 'start' command which will simply start the application.
/// Combined with the `waitForExit` parameter, this can be used to gracefully restart the application.
/// </summary>
/// <param name="waitForExit">If true, Update.exe will wait for the current process to exit before re-starting the application.</param>
/// <param name="locator">The locator to use to find the path to Update.exe and the packages directory.</param>
/// <param name="startArgs">The arguments to pass to the application when it is restarted.</param>
/// <param name="logger">The logger to use for diagnostic messages</param>
public static void Start(IVelopackLocator? locator = null, bool waitForExit = true, string[]? startArgs = null, ILogger? logger = null)
{
logger ??= NullLogger.Instance;
locator ??= VelopackLocator.GetDefault(logger);
var args = new List<string>();
args.Add("start");
if (waitForExit) {
args.Add("--waitPid");
args.Add(Process.GetCurrentProcess().Id.ToString());
}
if (startArgs != null && startArgs.Length > 0) {
args.Add("--");
foreach (var a in startArgs) {
args.Add(a);
}
}
StartUpdateExe(logger, locator, args);
}
/// <summary>
/// Runs Update.exe in the current working directory to apply updates, optionally restarting the application.
/// </summary>
@@ -38,12 +98,6 @@ namespace Velopack
logger ??= NullLogger.Instance;
locator ??= VelopackLocator.GetDefault(logger);
var psi = new ProcessStartInfo() {
CreateNoWindow = true,
FileName = locator.UpdateExePath,
WorkingDirectory = Path.GetDirectoryName(locator.UpdateExePath),
};
var args = new List<string>();
if (silent) args.Add("--silent");
args.Add("apply");
@@ -69,30 +123,13 @@ namespace Velopack
}
}
psi.AppendArgumentListSafe(args, out var debugArgs);
logger.Debug($"Restarting app to apply updates. Running: {psi.FileName} {debugArgs}");
var p = Process.Start(psi);
if (VelopackRuntimeInfo.IsWindows) {
if (p is not null) {
try {
// this is an attempt to work around a bug where the restarted app fails to come to foreground.
AllowSetForegroundWindow(p.Id);
} catch (Exception ex) {
logger.LogWarning(ex, "Failed to allow Update.exe to set foreground window.");
}
}
}
var p = StartUpdateExe(logger, locator, args);
Thread.Sleep(300);
if (p == null) {
throw new Exception("Failed to launch Update.exe process.");
}
if (p.HasExited) {
throw new Exception($"Update.exe process exited too soon ({p.ExitCode}).");
}
logger.Info("Update.exe apply triggered successfully.");
}
}
}