use anyhow::{Context, Result, ensure};
use std::{
ffi::OsString,
fs,
os::windows::{ffi::OsStringExt, process::CommandExt},
path::{Path, PathBuf},
process::{Command, Stdio},
};
const CLEANUP_SCRIPT: &str = r#"
$ErrorActionPreference = 'Stop'
$path = $env:MAGI_UPDATE_RELOCATED_IMAGE
$deadline = [DateTime]::UtcNow.AddHours(2)
do {
try {
[IO.File]::Delete($path)
[IO.Directory]::Delete([IO.Path]::GetDirectoryName($path))
exit 0
} catch {
[Threading.Thread]::Sleep(1000)
}
} while ([DateTime]::UtcNow -lt $deadline)
exit 1
"#;
pub(super) fn system_directory() -> Result<PathBuf> {
let mut buffer = vec![0u16; 32768];
let length = unsafe {
windows_sys::Win32::System::SystemInformation::GetSystemDirectoryW(
buffer.as_mut_ptr(),
buffer.len() as u32,
)
} as usize;
ensure!(
length > 0 && length < buffer.len(),
"cannot resolve Windows system directory"
);
let directory = PathBuf::from(OsString::from_wide(&buffer[..length]));
ensure!(
directory.is_absolute(),
"Windows system directory is not absolute"
);
Ok(directory)
}
fn cleanup_command(system: &Path, relocated: &Path) -> Command {
let mut command = Command::new(system.join("WindowsPowerShell/v1.0/powershell.exe"));
command
.args([
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-Command",
CLEANUP_SCRIPT,
])
.env("MAGI_UPDATE_RELOCATED_IMAGE", relocated)
.current_dir(system)
.creation_flags(0x08000000) .stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
command
}
pub(super) fn release_running_executable(executable: &Path, copy: &Path) -> Result<()> {
let system = system_directory()?;
ensure!(
system
.join("WindowsPowerShell/v1.0/powershell.exe")
.is_file(),
"Windows PowerShell is unavailable; update this installation manually"
);
let directory = tempfile::Builder::new()
.prefix(".magi-code-relocated-")
.tempdir_in(executable.parent().context("executable has no parent")?)?;
let replacement = directory.path().join("replacement.exe");
fs::copy(copy, &replacement).context("cannot stage Windows replacement")?;
let directory = directory.keep();
let relocated = directory.join("previous.exe");
fs::rename(executable, &relocated).context("cannot relocate running Windows image")?;
if let Err(error) = fs::rename(&replacement, executable) {
fs::rename(&relocated, executable).with_context(|| {
format!(
"cannot restore Windows image; recovery files: {}",
directory.display()
)
})?;
return Err(error).context("cannot restore the Windows install path");
}
cleanup_command(&system, &relocated)
.spawn()
.with_context(|| {
format!(
"cannot start Windows cleanup; recovery files: {}",
directory.display()
)
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cleanup_uses_absolute_system_program_and_keeps_filename_out_of_script() {
let system = system_directory().unwrap();
let path = Path::new(r"C:\untrusted & ' directory\previous.exe");
let command = cleanup_command(&system, path);
assert_eq!(
command.get_program(),
system.join("WindowsPowerShell/v1.0/powershell.exe")
);
assert_eq!(command.get_current_dir(), Some(system.as_path()));
assert_eq!(
command.get_args().last(),
Some(std::ffi::OsStr::new(CLEANUP_SCRIPT))
);
assert!(command.get_envs().any(|(key, value)| {
key == "MAGI_UPDATE_RELOCATED_IMAGE" && value == Some(path.as_os_str())
}));
}
#[test]
fn cleanup_removes_only_relocated_file_and_its_empty_directory() {
let root = tempfile::tempdir().unwrap();
let old_directory = root.path().join("old & ' image");
fs::create_dir(&old_directory).unwrap();
let old = old_directory.join("previous.exe");
fs::write(&old, b"old").unwrap();
let installed = root.path().join("magi-code.exe");
fs::write(&installed, b"new").unwrap();
let mut command = cleanup_command(&system_directory().unwrap(), &old);
let status =
crate::updates::process::run_cargo(&mut command, std::time::Duration::from_secs(15))
.unwrap();
assert!(status.success());
assert!(!old_directory.exists());
assert_eq!(fs::read(installed).unwrap(), b"new");
}
}