#[cfg(not(feature = "tracing"))]
use log::debug;
#[cfg(feature = "tracing")]
use tracing::debug;
use crate::{Error, RESUME_FLAG, UpdateState};
use human_errors::ResultExt;
use std::ffi::OsString;
use std::{path::Path, process::Command};
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(test)]
use mockall::automock;
#[cfg(windows)]
mod windows {
pub const DETACHED_PROCESS: u32 = 0x0000_0008;
pub const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
}
pub(crate) fn default() -> Box<dyn Launcher + Send + Sync> {
Box::new(DefaultLauncher {})
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Relaunch {
pub(crate) args: Vec<OsString>,
pub(crate) envs: Vec<(OsString, OsString)>,
}
#[cfg_attr(test, automock)]
pub trait Launcher {
fn launch(
&self,
app_path: &Path,
state: &UpdateState,
customization: &Relaunch,
) -> Result<(), Error> {
let state_json = serde_json::to_string(state).wrap_system_err(
"Failed to serialize the update state into a JSON payload.",
&["Please report this issue to the application's maintainers."],
)?;
debug!(
"Launching '{}' to perform the '{}' phase of the update.",
app_path.display(),
state.phase
);
let mut cmd = Command::new(app_path);
cmd.arg(RESUME_FLAG).arg(&state_json);
cmd.args(&customization.args);
cmd.envs(customization.envs.iter().map(|(k, v)| (k, v)));
#[cfg(windows)]
cmd.creation_flags(windows::DETACHED_PROCESS | windows::CREATE_NEW_PROCESS_GROUP);
self.spawn(cmd).wrap_system_err(
format!(
"Could not launch the new application version to continue the update process (-> {} phase).",
state.phase
),
&["Please report this issue to the application's maintainers, or try updating manually by downloading the latest release yourself."],
)
}
fn spawn(&self, cmd: Command) -> Result<(), Error>;
}
struct DefaultLauncher {}
impl Launcher for DefaultLauncher {
fn spawn(&self, mut cmd: Command) -> Result<(), Error> {
cmd.spawn().wrap_user_err(
"Failed to launch the application to complete the update process.",
&[
"Try re-running the update.",
"Download the latest release and install it manually if the problem continues.",
],
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::UpdatePhase;
use std::sync::Mutex;
struct CapturingLauncher {
captured: Mutex<Option<Command>>,
}
impl Launcher for CapturingLauncher {
fn spawn(&self, cmd: Command) -> Result<(), Error> {
*self.captured.lock().unwrap() = Some(cmd);
Ok(())
}
}
#[test]
fn launch_threads_custom_args_and_env_after_the_resume_flag() {
let launcher = CapturingLauncher {
captured: Mutex::new(None),
};
let state = UpdateState {
phase: UpdatePhase::Replace,
..Default::default()
};
let customization = Relaunch {
args: vec!["--trace-context".into(), "ctx-json".into()],
envs: vec![("APP_UPDATING".into(), "1".into())],
};
launcher
.launch(Path::new("app"), &state, &customization)
.expect("the capturing launcher never fails");
let cmd = launcher.captured.lock().unwrap().take().unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(args[0], RESUME_FLAG);
assert_eq!(&args[2..], &["--trace-context", "ctx-json"]);
let env: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.map(|v| v.to_string_lossy().into_owned()),
)
})
.collect();
assert!(
env.contains(&("APP_UPDATING".to_string(), Some("1".to_string()))),
"custom environment variable should be set on the relaunch: {env:?}"
);
}
#[test]
fn launch_without_customization_only_passes_the_resume_flag() {
let launcher = CapturingLauncher {
captured: Mutex::new(None),
};
let state = UpdateState {
phase: UpdatePhase::Cleanup,
..Default::default()
};
launcher
.launch(Path::new("app"), &state, &Relaunch::default())
.expect("the capturing launcher never fails");
let cmd = launcher.captured.lock().unwrap().take().unwrap();
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(args.len(), 2, "only the resume flag and state: {args:?}");
assert_eq!(args[0], RESUME_FLAG);
assert_eq!(cmd.get_envs().count(), 0, "no custom environment variables");
}
}