fix(updater): keep native helper dispatch and output in platform adapter
This commit is contained in:
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();
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
Reference in new issue
Block a user