Windows relaunches after an update (#1748)

After the MSI installer finishes, wait for msiexec and start the installed exe; relaunch is detached on Windows.
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-16 10:40:58 -04:00
1 parent a16226288f
commit 201e055259
4 files changed
+56 -6

No files matched your search

+14 -4
View File
@@ -94,10 +94,20 @@ impl eframe::App for App {
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_engine::crash::end_session();
// An update asked to restart into the new version or to run its installer.
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve)
&& let Err(e) = std::process::Command::new(&program).args(&args).spawn()
{
log::error!("update: starting {}: {e}", program.display());
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve) {
let mut command = std::process::Command::new(&program);
command.args(&args);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
// Not tied to this process: its own process group, and no console (the PowerShell
// that waits for an installer is hidden; the app itself is detached).
let hidden_shell = program.file_stem().is_some_and(|n| n.eq_ignore_ascii_case("powershell"));
command.creation_flags(0x0000_0200 | if hidden_shell { 0x0800_0000 } else { 0x0000_0008 });
}
if let Err(e) = command.spawn() {
log::error!("update: starting {}: {e}", program.display());
}
}
}
}
+7 -2
View File
@@ -697,8 +697,13 @@ fn poll_install(app: &mut SolveApp) {
app.updater.job = None;
match &result {
Ok(install::Outcome::Installer(program, args)) => {
// The installer replaces this program: start it once SolidCraft has closed.
app.updater.launch = Some((program.clone(), args.clone()));
// The installer replaces this program: start it once SolidCraft has closed. On
// Windows the installer is followed by the new version (see install::relaunch_after).
app.updater.launch = Some(match std::env::current_exe() {
#[cfg(windows)]
Ok(exe) => install::relaunch_after(program, args, &exe),
_ => (program.clone(), args.clone()),
});
crate::documents::request_quit(app);
}
Ok(install::Outcome::Opened(dmg)) => {
+20
View File
@@ -107,6 +107,26 @@ pub fn sha256_file(path: &Path) -> Result<String, String> {
Ok(h.finalize().iter().map(|b| format!("{b:02x}")).collect())
}
/// The command that runs an installer, waits for it to finish and then starts `exe` (the
/// installed program), so the app comes back after an update. Windows has no `wait && start` in
/// a plain process spawn, so PowerShell does the waiting; the installer's exit code is ignored on
/// purpose: when the user cancels it, the old version is still there to start.
pub fn relaunch_after(installer: &Path, args: &[String], exe: &Path) -> (PathBuf, Vec<String>) {
// PowerShell single-quoted string: only `'` needs doubling.
let q = |s: &str| format!("'{}'", s.replace('\'', "''"));
// msiexec splits its command line itself, so an argument with spaces needs its own quotes.
let list = args.iter().map(|a| if a.contains(' ') { q(&format!("\"{a}\"")) } else { q(a) }).collect::<Vec<_>>().join(",");
let script = format!(
"Start-Process -FilePath {} -ArgumentList {list} -Wait; Start-Process -FilePath {}",
q(&installer.to_string_lossy()),
q(&exe.to_string_lossy())
);
(
PathBuf::from("powershell"),
vec!["-NoProfile".into(), "-NonInteractive".into(), "-WindowStyle".into(), "Hidden".into(), "-Command".into(), script],
)
}
/// `sums` is signed by `key` (minisign, prehashed) with a trusted comment naming `tag`.
pub fn verify_signature(sums: &[u8], signature: &str, key: &str, tag: &str) -> Result<(), String> {
let key = minisign_verify::PublicKey::from_base64(key.trim()).map_err(|e| format!("update signing key: {e}"))?;
@@ -203,3 +203,18 @@ fn installers_and_disk_images_are_handed_over() {
assert_eq!(apply(&Method::Dmg, &pkg).unwrap(), Outcome::Opened(pkg.clone()));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn windows_installer_is_followed_by_the_new_version() {
let args = vec!["/i".to_string(), r"C:\Users\a b\Temp\solidcraft-update\x.msi".to_string(), "/passive".to_string()];
let (program, out) = relaunch_after(Path::new("msiexec"), &args, Path::new(r"C:\Program Files\SolidCraft\it's.exe"));
assert_eq!(program, PathBuf::from("powershell"));
assert_eq!(
out.last().unwrap(),
&concat!(
r#"Start-Process -FilePath 'msiexec' -ArgumentList '/i','"C:\Users\a b\Temp\solidcraft-update\x.msi"','/passive' -Wait; "#,
r"Start-Process -FilePath 'C:\Program Files\SolidCraft\it''s.exe'"
)
);
assert!(out.iter().any(|a| a == "-Command"));
}