#[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::new())
}
#[cfg_attr(test, automock)]
pub trait Launcher {
fn resume_args(&self, state_json: &str) -> Vec<OsString> {
vec![RESUME_FLAG.into(), state_json.into()]
}
fn extra_args(&self) -> Vec<OsString> {
Vec::new()
}
fn extra_envs(&self) -> Vec<(OsString, OsString)> {
Vec::new()
}
fn launch(&self, app_path: &Path, state: &UpdateState) -> 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.args(self.resume_args(&state_json));
cmd.args(self.extra_args());
cmd.envs(self.extra_envs());
self.detach(&mut cmd);
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 detach(&self, cmd: &mut Command) {
#[cfg(windows)]
cmd.creation_flags(windows::DETACHED_PROCESS | windows::CREATE_NEW_PROCESS_GROUP);
#[cfg(not(windows))]
let _ = cmd;
}
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(())
}
}
#[derive(Debug, Default, Clone)]
pub struct DefaultLauncher {
args: Vec<OsString>,
envs: Vec<(OsString, OsString)>,
}
impl DefaultLauncher {
pub fn new() -> Self {
Self::default()
}
pub fn with_arg(mut self, arg: impl Into<OsString>) -> Self {
self.args.push(arg.into());
self
}
pub fn with_args<I, A>(mut self, args: I) -> Self
where
I: IntoIterator<Item = A>,
A: Into<OsString>,
{
self.args.extend(args.into_iter().map(Into::into));
self
}
pub fn with_env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.envs.push((key.into(), value.into()));
self
}
pub fn with_envs<I, K, V>(mut self, envs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
{
self.envs
.extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
}
impl Launcher for DefaultLauncher {
fn extra_args(&self) -> Vec<OsString> {
self.args.clone()
}
fn extra_envs(&self) -> Vec<(OsString, OsString)> {
self.envs.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::UpdatePhase;
use std::sync::Mutex;
struct CapturingLauncher {
captured: Mutex<Option<Command>>,
}
impl CapturingLauncher {
fn new() -> Self {
Self {
captured: Mutex::new(None),
}
}
fn args(&self) -> Vec<String> {
self.captured
.lock()
.unwrap()
.as_ref()
.unwrap()
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect()
}
fn env(&self) -> Vec<(String, Option<String>)> {
self.captured
.lock()
.unwrap()
.as_ref()
.unwrap()
.get_envs()
.map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.map(|v| v.to_string_lossy().into_owned()),
)
})
.collect()
}
}
impl Launcher for CapturingLauncher {
fn spawn(&self, cmd: Command) -> Result<(), Error> {
*self.captured.lock().unwrap() = Some(cmd);
Ok(())
}
}
#[test]
fn default_launch_passes_the_resume_flag_and_state() {
let launcher = CapturingLauncher::new();
let state = UpdateState {
phase: UpdatePhase::Replace,
..Default::default()
};
launcher
.launch(Path::new("app"), &state)
.expect("the capturing launcher never fails");
let args = launcher.args();
assert_eq!(args.len(), 2, "only the resume flag and state: {args:?}");
assert_eq!(args[0], RESUME_FLAG);
assert!(
args[1].contains("\"phase\":\"replace\""),
"the serialized state should follow the resume flag: {args:?}"
);
assert!(
launcher.env().is_empty(),
"the default launch sets no environment variables"
);
}
struct SubcommandLauncher {
inner: CapturingLauncher,
}
impl Launcher for SubcommandLauncher {
fn resume_args(&self, state_json: &str) -> Vec<OsString> {
vec!["update".into(), "--state".into(), state_json.into()]
}
fn spawn(&self, cmd: Command) -> Result<(), Error> {
self.inner.spawn(cmd)
}
}
#[test]
fn launch_honours_a_custom_resume_args_convention() {
let launcher = SubcommandLauncher {
inner: CapturingLauncher::new(),
};
let state = UpdateState {
phase: UpdatePhase::Replace,
..Default::default()
};
launcher
.launch(Path::new("app"), &state)
.expect("the capturing launcher never fails");
let args = launcher.inner.args();
assert_eq!(&args[..2], &["update", "--state"]);
assert!(
args[2].contains("\"phase\":\"replace\""),
"the serialized state should follow the sub-command: {args:?}"
);
assert!(
!args.iter().any(|a| a == RESUME_FLAG),
"the default resume flag should have been replaced: {args:?}"
);
}
struct ExtraLauncher {
inner: CapturingLauncher,
}
impl Launcher for ExtraLauncher {
fn extra_args(&self) -> Vec<OsString> {
vec!["--updated".into()]
}
fn extra_envs(&self) -> Vec<(OsString, OsString)> {
vec![("APP_UPDATING".into(), "1".into())]
}
fn spawn(&self, cmd: Command) -> Result<(), Error> {
self.inner.spawn(cmd)
}
}
#[test]
fn launch_appends_extra_args_and_env_after_the_resume_flag() {
let launcher = ExtraLauncher {
inner: CapturingLauncher::new(),
};
let state = UpdateState {
phase: UpdatePhase::Replace,
..Default::default()
};
launcher
.launch(Path::new("app"), &state)
.expect("the capturing launcher never fails");
let args = launcher.inner.args();
assert_eq!(args[0], RESUME_FLAG);
assert_eq!(args[2], "--updated");
assert!(
launcher
.inner
.env()
.contains(&("APP_UPDATING".to_string(), Some("1".to_string()))),
"the extra environment variable should be set on the relaunch"
);
}
#[test]
fn default_launcher_builders_collect_args_and_env() {
let launcher = DefaultLauncher::new()
.with_arg("--updated")
.with_args(["--from", "v1"])
.with_env("APP_UPDATING", "1")
.with_envs([("CHANNEL", "beta")]);
assert_eq!(
launcher.extra_args(),
[
OsString::from("--updated"),
OsString::from("--from"),
OsString::from("v1")
]
);
assert_eq!(
launcher.extra_envs(),
[
(OsString::from("APP_UPDATING"), OsString::from("1")),
(OsString::from("CHANNEL"), OsString::from("beta")),
]
);
}
}