use std::fs;
use std::path::Path;
use super::Installed;
use crate::error::{Error, Result};
pub(crate) fn install(artifact: &Path, install_root: &Path) -> Result<Installed> {
match artifact
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.as_deref()
{
Some("exe") => install_bare_exe(artifact, install_root),
Some("msi") => stage_msi(artifact, install_root),
_ => Err(Error::UnsupportedPlatform(
"windows update artifact (expected .exe or .msi)",
)),
}
}
fn install_bare_exe(new_exe: &Path, install_root: &Path) -> Result<Installed> {
let root_is_exe = install_root
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("exe"));
if !root_is_exe {
return Err(Error::Install(format!(
"bare-exe update needs an .exe install root, got {}",
install_root.display()
)));
}
let old = install_root.with_extension("old.exe");
let _ = fs::remove_file(&old);
if install_root.exists() {
fs::rename(install_root, &old)
.map_err(|e| Error::Install(format!("could not rename running exe: {e}")))?;
}
if let Err(e) = fs::copy(new_exe, install_root) {
if old.exists() {
let _ = fs::rename(&old, install_root);
}
return Err(Error::Install(format!("could not place new exe: {e}")));
}
Ok(Installed {
restart_path: Some(install_root.to_path_buf()),
})
}
fn stage_msi(msi: &Path, install_root: &Path) -> Result<Installed> {
let script_path = msi.with_extension("apply.cmd");
fs::write(&script_path, apply_script(msi, install_root)?)
.map_err(|e| Error::Install(format!("could not stage MSI apply script: {e}")))?;
Ok(Installed {
restart_path: Some(script_path),
})
}
fn apply_script(msi: &Path, relaunch: &Path) -> Result<String> {
let msi = cmd_quoted(msi)?;
let relaunch = cmd_quoted(relaunch)?;
Ok(format!(
"@echo off\r\n\
rem Generated by gpui-updater: applies the staged MSI update and\r\n\
rem relaunches the app. Run automatically after the app exits.\r\n\
msiexec /i {msi} /passive /norestart\r\n\
if %errorlevel% neq 0 exit /b %errorlevel%\r\n\
start \"\" {relaunch}\r\n"
))
}
fn cmd_quoted(path: &Path) -> Result<String> {
let s = path
.to_str()
.ok_or_else(|| Error::Install(format!("non-UTF-8 path: {}", path.display())))?;
if s.contains('"') || s.contains('%') || s.contains('\r') || s.contains('\n') {
return Err(Error::Install(format!(
"path cannot be quoted for cmd.exe: {s}"
)));
}
Ok(format!("\"{s}\""))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn dispatch_rejects_unknown_artifact_kinds() {
let err = install(Path::new("C:\\dl\\app.zip"), Path::new("C:\\app\\app.exe")).unwrap_err();
assert!(matches!(err, Error::UnsupportedPlatform(_)));
}
#[test]
fn bare_exe_requires_an_exe_install_root() {
let err = install(Path::new("C:\\dl\\app.exe"), Path::new("C:\\app")).unwrap_err();
assert!(matches!(err, Error::Install(_)));
}
#[test]
fn exe_rename_trick_swaps_and_keeps_the_old_copy() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("app.exe");
let new = dir.path().join("download.exe");
fs::write(&root, b"old").unwrap();
fs::write(&new, b"new").unwrap();
let installed = install(&new, &root).unwrap();
assert_eq!(installed.restart_path.as_deref(), Some(root.as_path()));
assert_eq!(fs::read(&root).unwrap(), b"new");
assert_eq!(fs::read(root.with_extension("old.exe")).unwrap(), b"old");
}
#[test]
fn msi_stages_an_apply_script_as_the_restart_path() {
let dir = tempfile::tempdir().unwrap();
let msi = dir.path().join("App-v2.msi");
fs::write(&msi, b"msi").unwrap();
let root = dir.path().join("App.exe");
let installed = install(&msi, &root).unwrap();
let script_path = installed.restart_path.unwrap();
assert_eq!(script_path, msi.with_extension("apply.cmd"));
let script = fs::read_to_string(script_path).unwrap();
assert!(script.contains(&format!(
"msiexec /i \"{}\" /passive /norestart",
msi.display()
)));
assert!(script.contains(&format!("start \"\" \"{}\"", root.display())));
assert!(script.contains("if %errorlevel% neq 0 exit /b %errorlevel%"));
assert_eq!(fs::read(&msi).unwrap(), b"msi");
}
#[test]
fn quoting_refuses_paths_cmd_cannot_carry() {
for bad in ["C:\\a\"b\\x.msi", "C:\\a%b\\x.msi"] {
assert!(matches!(
apply_script(Path::new(bad), Path::new("C:\\app\\App.exe")),
Err(Error::Install(_))
));
}
}
}