mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Add re/start command for linux & mac (#158)
* Add start command for linux/mac * Add start command to UpdateExe
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(¤t).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(¤t);
|
||||
|
||||
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(¤t_dir).exists() {
|
||||
info!("Renaming latest app-* folder to current.");
|
||||
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(&root_dir)? {
|
||||
fs::rename(&latest_app_dir, ¤t_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(())
|
||||
}
|
||||
|
||||
106
src/Rust/src/commands/start_windows_impl.rs
Normal file
106
src/Rust/src/commands/start_windows_impl.rs
Normal 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(¤t).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(¤t);
|
||||
|
||||
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(¤t_dir).exists() {
|
||||
info!("Renaming latest app-* folder to current.");
|
||||
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(root_dir)? {
|
||||
fs::rename(latest_app_dir, ¤t_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(())
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user