fix(updater): keep native helper dispatch and output in platform adapter

This commit is contained in:
kuddev committed 2026-09-24 12:09:20 +08:00
1 parent 3af0e5face
commit 22d36b1191
5 files changed
+64 -28

No files matched your search

@@ -58,6 +58,14 @@ this bounded launch check; the retained backup and durable workspace remain.
Cancelled or rejected transactions keep the original application running. Helpers
have bounded preparation, commit and exit waits and never kill the user's process.
The platform adapter also owns helper materialization and launch selection. The
shared transaction requests a helper through one platform-independent entry,
without adding operating-system branches to prepare/commit orchestration.
Native command output is spooled to temporary files while enforcing the existing
process deadline, then read with a 2 MiB limit per stream. Polling a child before
reading piped output can otherwise block both processes once a pipe fills. A
native regression produces 256 KiB on each stream and checks command failure.
## Rejected alternatives
- Replacing a running bundle: risks mixed resources and incomplete session saves.
@@ -105,3 +105,16 @@ pub(crate) fn guard_base(executable: &Path) -> PathBuf {
}
executable.parent().expect("canonical executable parent").join(".pebrel-update")
}
/// Select and materialize the native helper; transaction authority stays with the caller.
pub(crate) fn spawn_prepared_helper(directory: &Path, plan: &Path) -> Result<Child, String> {
#[cfg(target_os = "macos")]
return macos::spawn(directory, plan);
#[cfg(not(target_os = "macos"))]
{
let helper = directory.join("handoff.ps1");
crate::atomic_file::write(&helper, include_bytes!("../update_download/handoff.ps1"))
.map_err(|error| error.to_string())?;
spawn_helper(&helper, plan).map_err(|error| error.to_string())
}
}
@@ -3,14 +3,31 @@ use std::ffi::{CStr, CString};
use std::io;
use std::os::unix::ffi::OsStrExt as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};
pub(crate) fn spawn(directory: &Path, plan: &Path) -> Result<Child, String> {
let helper = directory.join("handoff");
std::fs::copy(std::env::current_exe().map_err(|e| e.to_string())?, &helper)
.map_err(|e| e.to_string())?;
Command::new(helper)
.arg("--internal-macos-update")
.arg(plan)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| e.to_string())
}
pub(crate) fn run(command: &mut Command) -> Result<Output, String> {
// Regular files cannot fill a pipe while we poll for process completion.
let stdout = tempfile::NamedTempFile::new().map_err(|e| e.to_string())?;
let stderr = tempfile::NamedTempFile::new().map_err(|e| e.to_string())?;
let mut child = command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdout(stdout.reopen().map_err(|e| e.to_string())?)
.stderr(stderr.reopen().map_err(|e| e.to_string())?)
.spawn()
.map_err(|e| e.to_string())?;
let deadline = Instant::now() + Duration::from_secs(120);
@@ -22,7 +39,17 @@ pub(crate) fn run(command: &mut Command) -> Result<Output, String> {
}
std::thread::sleep(Duration::from_millis(25));
}
let output = child.wait_with_output().map_err(|e| e.to_string())?;
let read_output = |file: &tempfile::NamedTempFile| -> Result<Vec<u8>, String> {
if file.as_file().metadata().map_err(|e| e.to_string())?.len() > 2 * 1024 * 1024 {
return Err("macOS update command output exceeded its limit".into());
}
std::fs::read(file.path()).map_err(|e| e.to_string())
};
let output = Output {
status: child.wait().map_err(|e| e.to_string())?,
stdout: read_output(&stdout)?,
stderr: read_output(&stderr)?,
};
if !output.status.success() {
return Err(format!(
"macOS update command failed ({}): {}",
@@ -223,6 +250,16 @@ pub(crate) fn stage(
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_output_larger_than_a_pipe_is_drained() {
let output = run(Command::new("/bin/sh")
.args(["-c", "head -c 262144 /dev/zero; head -c 262144 /dev/zero >&2"]))
.unwrap();
assert_eq!(output.stdout.len(), 262144);
assert_eq!(output.stderr.len(), 262144);
assert!(run(Command::new("/bin/sh").args(["-c", "exit 7"])).is_err());
}
#[test]
fn atomic_exchange_keeps_a_complete_original_and_can_roll_back() {
let dir = tempfile::tempdir().unwrap();
+2 -10
View File
@@ -162,16 +162,8 @@ pub(crate) fn prepare(asset: &UpdateAsset) -> Result<PreparedUpdate, String> {
&serde_json::to_vec(&plan_path).map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())?;
#[cfg(target_os = "macos")]
let child = macos::spawn(&directory, &plan_path)?;
#[cfg(not(target_os = "macos"))]
let child = {
let helper = directory.join("handoff.ps1");
crate::atomic_file::write(&helper, include_bytes!("handoff.ps1"))
.map_err(|error| error.to_string())?;
crate::platform::update_installation::spawn_helper(&helper, &plan_path)
.map_err(|error| error.to_string())?
};
let child =
crate::platform::update_installation::spawn_prepared_helper(&directory, &plan_path)?;
let mut prepared = PreparedUpdate { directory, transaction, child, committed: false };
let timeout = if crate::platform::Platform::current() == crate::platform::Platform::MacOS {
600
@@ -20,20 +20,6 @@ pub(crate) fn run_helper_if_requested() -> Option<i32> {
})
}
pub(crate) fn spawn(directory: &Path, plan: &Path) -> Result<Child, String> {
let helper = directory.join("handoff");
std::fs::copy(std::env::current_exe().map_err(|e| e.to_string())?, &helper)
.map_err(|e| e.to_string())?;
Command::new(helper)
.arg("--internal-macos-update")
.arg(plan)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| e.to_string())
}
fn run(path: &Path) -> Result<(), String> {
let path = canonical(path).map_err(|e| e.to_string())?;
let directory = path.parent().ok_or("Missing update directory")?;