fix(updater): keep portable replacement on target volume

Closes #10169
This commit is contained in:
t8y2
2026-09-27 08:14:01 +00:00
parent bee32d46b5
commit e03565be66
+45 -5
View File
@@ -170,6 +170,11 @@ fn sha256_hex(bytes: &[u8]) -> String {
Sha256::digest(bytes).iter().map(|byte| format!("{byte:02x}")).collect()
}
#[cfg(any(target_os = "windows", test))]
fn portable_update_staging_dir(executable_dir: &std::path::Path, update_id: &str) -> std::path::PathBuf {
executable_dir.join(format!(".dbx-portable-update-{update_id}"))
}
/// Windows PowerShell lives in a versioned directory that Rust's executable
/// search order never visits (it only probes the `System32` root), so a machine
/// whose `PATH` lost that entry fails with "program not found" even though
@@ -215,7 +220,10 @@ pub(super) fn launch_portable_update_helper(archive: &[u8], version: &Version) -
fs::remove_file(&write_probe)
.map_err(|error| format!("Failed to finish portable directory write check: {error}"))?;
let staging_dir = std::env::temp_dir().join(format!("dbx-portable-update-{update_id}"));
// Keep the replacement and helper on the target volume. Windows
// PowerShell cannot reliably Move-Item an executable across volumes after
// the running target has already been renamed to its backup.
let staging_dir = portable_update_staging_dir(exe_dir, &update_id);
fs::create_dir(&staging_dir)
.map_err(|error| format!("Failed to create portable update staging directory: {error}"))?;
let staged_exe = staging_dir.join("DBX.exe.new");
@@ -273,7 +281,7 @@ pub(super) fn launch_portable_update_helper(_archive: &[u8], _version: &Version)
Err("Portable updates are only supported on Windows.".to_string())
}
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", test))]
const PORTABLE_UPDATE_SCRIPT: &str = r#"param(
[Parameter(Mandatory = $true)][int]$ParentProcessId,
[Parameter(Mandatory = $true)][string]$SourceExe,
@@ -337,9 +345,9 @@ exit 0
#[cfg(test)]
mod tests {
use super::{
decode_tauri_text, portable_asset_name, sha256_hex, validate_requested_portable_version,
validated_portable_executable, PORTABLE_EXECUTABLE_NAME, PORTABLE_UPDATE_MANIFEST_NAME,
PORTABLE_UPDATE_MANIFEST_SCHEMA_VERSION,
decode_tauri_text, portable_asset_name, portable_update_staging_dir, sha256_hex,
validate_requested_portable_version, validated_portable_executable, PORTABLE_EXECUTABLE_NAME,
PORTABLE_UPDATE_MANIFEST_NAME, PORTABLE_UPDATE_MANIFEST_SCHEMA_VERSION, PORTABLE_UPDATE_SCRIPT,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use semver::Version;
@@ -371,6 +379,38 @@ mod tests {
assert!(portable_asset_name("../../0.5.64", "x86_64").is_err());
}
#[test]
fn stages_portable_replacement_beside_the_target() {
let executable_dir = std::path::Path::new("portable-install");
let staging_dir = portable_update_staging_dir(executable_dir, "123-456");
assert_eq!(staging_dir.parent(), Some(executable_dir));
assert_eq!(staging_dir.file_name().and_then(|name| name.to_str()), Some(".dbx-portable-update-123-456"));
assert_eq!(staging_dir.join("DBX.exe.new").parent(), Some(staging_dir.as_path()));
assert_eq!(staging_dir.join("apply-update.ps1").parent(), Some(staging_dir.as_path()));
}
#[test]
fn portable_update_script_preserves_replacement_and_rollback_order() {
let backup = PORTABLE_UPDATE_SCRIPT
.find("Move-Item -LiteralPath $TargetExe -Destination $BackupExe -Force")
.expect("script must back up the running executable");
let replacement = PORTABLE_UPDATE_SCRIPT
.find("Move-Item -LiteralPath $SourceExe -Destination $TargetExe -Force")
.expect("script must install the staged executable");
let relaunch = PORTABLE_UPDATE_SCRIPT
.find("Start-Process -FilePath $TargetExe")
.expect("script must relaunch the portable executable");
let cleanup = PORTABLE_UPDATE_SCRIPT
.find("Remove-Item -LiteralPath $StagingDir -Recurse -Force")
.expect("script must clean the staging directory after relaunch");
assert!(backup < replacement);
assert!(replacement < relaunch);
assert!(relaunch < cleanup);
assert!(PORTABLE_UPDATE_SCRIPT.contains("Copy-Item -LiteralPath $BackupExe -Destination $TargetExe -Force"));
}
#[test]
fn accepts_newer_portable_update_requests() {
assert_eq!(validate_requested_portable_version("0.5.64", "0.5.63").unwrap(), Version::parse("0.5.64").unwrap());