1#[cfg(not(feature = "tracing"))]
2use log::debug;
3#[cfg(feature = "tracing")]
4use tracing::debug;
5
6use crate::{Error, RESUME_FLAG, UpdateState};
7use human_errors::ResultExt;
8use std::ffi::OsString;
9use std::{path::Path, process::Command};
10
11#[cfg(windows)]
12use std::os::windows::process::CommandExt;
13
14#[cfg(test)]
15use mockall::automock;
16
17#[cfg(windows)]
18mod windows {
19 pub const DETACHED_PROCESS: u32 = 0x0000_0008;
22 pub const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
23}
24
25pub(crate) fn default() -> Box<dyn Launcher + Send + Sync> {
26 Box::new(DefaultLauncher::new())
27}
28
29#[cfg_attr(test, automock)]
48pub trait Launcher {
49 fn resume_args(&self, state_json: &str) -> Vec<OsString> {
71 vec![RESUME_FLAG.into(), state_json.into()]
72 }
73
74 fn extra_args(&self) -> Vec<OsString> {
80 Vec::new()
81 }
82
83 fn extra_envs(&self) -> Vec<(OsString, OsString)> {
89 Vec::new()
90 }
91
92 fn launch(&self, app_path: &Path, state: &UpdateState) -> Result<(), Error> {
103 let state_json = serde_json::to_string(state).wrap_system_err(
104 "Failed to serialize the update state into a JSON payload.",
105 &["Please report this issue to the application's maintainers."],
106 )?;
107
108 debug!(
109 "Launching '{}' to perform the '{}' phase of the update.",
110 app_path.display(),
111 state.phase
112 );
113
114 let mut cmd = Command::new(app_path);
115 cmd.args(self.resume_args(&state_json));
116 cmd.args(self.extra_args());
117 cmd.envs(self.extra_envs());
118 self.detach(&mut cmd);
119
120 self.spawn(cmd).wrap_system_err(
121 format!(
122 "Could not launch the new application version to continue the update process (-> {} phase).",
123 state.phase
124 ),
125 &["Please report this issue to the application's maintainers, or try updating manually by downloading the latest release yourself."],
126 )
127 }
128
129 fn detach(&self, cmd: &mut Command) {
134 #[cfg(windows)]
135 cmd.creation_flags(windows::DETACHED_PROCESS | windows::CREATE_NEW_PROCESS_GROUP);
136 #[cfg(not(windows))]
137 let _ = cmd;
138 }
139
140 fn spawn(&self, mut cmd: Command) -> Result<(), Error> {
144 cmd.spawn().wrap_user_err(
145 "Failed to launch the application to complete the update process.",
146 &[
147 "Try re-running the update.",
148 "Download the latest release and install it manually if the problem continues.",
149 ],
150 )?;
151
152 Ok(())
153 }
154}
155
156#[derive(Debug, Default, Clone)]
173pub struct DefaultLauncher {
174 args: Vec<OsString>,
175 envs: Vec<(OsString, OsString)>,
176}
177
178impl DefaultLauncher {
179 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub fn with_arg(mut self, arg: impl Into<OsString>) -> Self {
187 self.args.push(arg.into());
188 self
189 }
190
191 pub fn with_args<I, A>(mut self, args: I) -> Self
193 where
194 I: IntoIterator<Item = A>,
195 A: Into<OsString>,
196 {
197 self.args.extend(args.into_iter().map(Into::into));
198 self
199 }
200
201 pub fn with_env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
204 self.envs.push((key.into(), value.into()));
205 self
206 }
207
208 pub fn with_envs<I, K, V>(mut self, envs: I) -> Self
210 where
211 I: IntoIterator<Item = (K, V)>,
212 K: Into<OsString>,
213 V: Into<OsString>,
214 {
215 self.envs
216 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
217 self
218 }
219}
220
221impl Launcher for DefaultLauncher {
222 fn extra_args(&self) -> Vec<OsString> {
223 self.args.clone()
224 }
225
226 fn extra_envs(&self) -> Vec<(OsString, OsString)> {
227 self.envs.clone()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::UpdatePhase;
235 use std::sync::Mutex;
236
237 struct CapturingLauncher {
240 captured: Mutex<Option<Command>>,
241 }
242
243 impl CapturingLauncher {
244 fn new() -> Self {
245 Self {
246 captured: Mutex::new(None),
247 }
248 }
249
250 fn args(&self) -> Vec<String> {
251 self.captured
252 .lock()
253 .unwrap()
254 .as_ref()
255 .unwrap()
256 .get_args()
257 .map(|a| a.to_string_lossy().into_owned())
258 .collect()
259 }
260
261 fn env(&self) -> Vec<(String, Option<String>)> {
262 self.captured
263 .lock()
264 .unwrap()
265 .as_ref()
266 .unwrap()
267 .get_envs()
268 .map(|(k, v)| {
269 (
270 k.to_string_lossy().into_owned(),
271 v.map(|v| v.to_string_lossy().into_owned()),
272 )
273 })
274 .collect()
275 }
276 }
277
278 impl Launcher for CapturingLauncher {
279 fn spawn(&self, cmd: Command) -> Result<(), Error> {
280 *self.captured.lock().unwrap() = Some(cmd);
281 Ok(())
282 }
283 }
284
285 #[test]
286 fn default_launch_passes_the_resume_flag_and_state() {
287 let launcher = CapturingLauncher::new();
288 let state = UpdateState {
289 phase: UpdatePhase::Replace,
290 ..Default::default()
291 };
292
293 launcher
294 .launch(Path::new("app"), &state)
295 .expect("the capturing launcher never fails");
296
297 let args = launcher.args();
298 assert_eq!(args.len(), 2, "only the resume flag and state: {args:?}");
299 assert_eq!(args[0], RESUME_FLAG);
300 assert!(
301 args[1].contains("\"phase\":\"replace\""),
302 "the serialized state should follow the resume flag: {args:?}"
303 );
304 assert!(
305 launcher.env().is_empty(),
306 "the default launch sets no environment variables"
307 );
308 }
309
310 struct SubcommandLauncher {
313 inner: CapturingLauncher,
314 }
315
316 impl Launcher for SubcommandLauncher {
317 fn resume_args(&self, state_json: &str) -> Vec<OsString> {
318 vec!["update".into(), "--state".into(), state_json.into()]
319 }
320
321 fn spawn(&self, cmd: Command) -> Result<(), Error> {
322 self.inner.spawn(cmd)
323 }
324 }
325
326 #[test]
327 fn launch_honours_a_custom_resume_args_convention() {
328 let launcher = SubcommandLauncher {
329 inner: CapturingLauncher::new(),
330 };
331 let state = UpdateState {
332 phase: UpdatePhase::Replace,
333 ..Default::default()
334 };
335
336 launcher
337 .launch(Path::new("app"), &state)
338 .expect("the capturing launcher never fails");
339
340 let args = launcher.inner.args();
341 assert_eq!(&args[..2], &["update", "--state"]);
342 assert!(
343 args[2].contains("\"phase\":\"replace\""),
344 "the serialized state should follow the sub-command: {args:?}"
345 );
346 assert!(
347 !args.iter().any(|a| a == RESUME_FLAG),
348 "the default resume flag should have been replaced: {args:?}"
349 );
350 }
351
352 struct ExtraLauncher {
356 inner: CapturingLauncher,
357 }
358
359 impl Launcher for ExtraLauncher {
360 fn extra_args(&self) -> Vec<OsString> {
361 vec!["--updated".into()]
362 }
363
364 fn extra_envs(&self) -> Vec<(OsString, OsString)> {
365 vec![("APP_UPDATING".into(), "1".into())]
366 }
367
368 fn spawn(&self, cmd: Command) -> Result<(), Error> {
369 self.inner.spawn(cmd)
370 }
371 }
372
373 #[test]
374 fn launch_appends_extra_args_and_env_after_the_resume_flag() {
375 let launcher = ExtraLauncher {
376 inner: CapturingLauncher::new(),
377 };
378 let state = UpdateState {
379 phase: UpdatePhase::Replace,
380 ..Default::default()
381 };
382
383 launcher
384 .launch(Path::new("app"), &state)
385 .expect("the capturing launcher never fails");
386
387 let args = launcher.inner.args();
388 assert_eq!(args[0], RESUME_FLAG);
390 assert_eq!(args[2], "--updated");
391 assert!(
392 launcher
393 .inner
394 .env()
395 .contains(&("APP_UPDATING".to_string(), Some("1".to_string()))),
396 "the extra environment variable should be set on the relaunch"
397 );
398 }
399
400 #[test]
401 fn default_launcher_builders_collect_args_and_env() {
402 let launcher = DefaultLauncher::new()
403 .with_arg("--updated")
404 .with_args(["--from", "v1"])
405 .with_env("APP_UPDATING", "1")
406 .with_envs([("CHANNEL", "beta")]);
407
408 assert_eq!(
409 launcher.extra_args(),
410 [
411 OsString::from("--updated"),
412 OsString::from("--from"),
413 OsString::from("v1")
414 ]
415 );
416 assert_eq!(
417 launcher.extra_envs(),
418 [
419 (OsString::from("APP_UPDATING"), OsString::from("1")),
420 (OsString::from("CHANNEL"), OsString::from("beta")),
421 ]
422 );
423 }
424}