Skip to content

Commit c3e1f38

Browse files
committed
fix(manager): serialize module spawns so pipes don't leak
On macOS, std creates a child's stdio pipes with pipe() and only then sets close-on-exec (Linux uses pipe2(O_CLOEXEC)). Modules are spawned from their own threads, so at startup one module could inherit another module's pipe ends. When the owning module exited, the pipe stayed open, its reader never saw EOF, and Stopped wasn't sent until the other module exited too, leaving a stale PID in the manager and no crash restart. This predates the stderr capture, but piping stderr doubled the pipes per module. Take a lock around spawn() so pipe creation and exec can't interleave across module threads. The new stress test lost a Stopped message in 2 of 3 runs without the lock and passes consistently with it.
1 parent 1da317b commit c3e1f38

1 file changed

Lines changed: 61 additions & 9 deletions

File tree

‎src-tauri/src/manager.rs‎

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -760,10 +760,13 @@ fn start_generic_module_thread(
760760

761761
// Pipe stderr too: it's where modules report why they crashed, and when inherited it went
762762
// to aw-tauri's own stderr, which is /dev/null for a GUI app.
763-
let child = command
764-
.stdout(std::process::Stdio::piped())
765-
.stderr(std::process::Stdio::piped())
766-
.spawn();
763+
let child = {
764+
let _guard = SPAWN_LOCK.lock().unwrap_or_else(|e| e.into_inner());
765+
command
766+
.stdout(std::process::Stdio::piped())
767+
.stderr(std::process::Stdio::piped())
768+
.spawn()
769+
};
767770

768771
let mut child = match child {
769772
Ok(c) => c,
@@ -880,11 +883,14 @@ fn start_notify_module_thread(
880883
#[cfg(windows)]
881884
command.creation_flags(CREATE_NO_WINDOW);
882885

883-
let mut child = match command
884-
.stdout(std::process::Stdio::piped())
885-
.stderr(std::process::Stdio::piped())
886-
.spawn()
887-
{
886+
let child = {
887+
let _guard = SPAWN_LOCK.lock().unwrap_or_else(|e| e.into_inner());
888+
command
889+
.stdout(std::process::Stdio::piped())
890+
.stderr(std::process::Stdio::piped())
891+
.spawn()
892+
};
893+
let mut child = match child {
888894
Ok(child) => child,
889895
Err(e) => {
890896
// An unsupported --output-only is detected after exit (below), not here: spawn
@@ -1017,6 +1023,12 @@ fn start_notify_module_thread(
10171023
});
10181024
}
10191025

1026+
/// Serializes module spawns. On macOS, std creates a child's stdio pipes with pipe() and only then
1027+
/// marks them close-on-exec, so a module spawned at the same moment from another thread can
1028+
/// inherit them. The pipe then stays open after its module exits, the reader never sees EOF, and
1029+
/// the Stopped message is delayed until the other module exits too.
1030+
static SPAWN_LOCK: Mutex<()> = Mutex::new(());
1031+
10201032
/// How much of a module's stdout/stderr to keep for crash diagnostics.
10211033
const OUTPUT_TAIL_BYTES: usize = 64 * 1024;
10221034

@@ -1377,6 +1389,46 @@ mod tests {
13771389
}
13781390
}
13791391

1392+
/// Starting modules concurrently must not leak one module's output pipes into another:
1393+
/// if a long-running module inherits them, the short one never reports Stopped.
1394+
#[cfg(unix)]
1395+
#[test]
1396+
fn concurrent_spawns_dont_delay_stopped() {
1397+
let (tx, rx) = channel();
1398+
let sh = |script: &str, tx: &Sender<ModuleMessage>| {
1399+
let args = vec!["-c".to_string(), script.to_string()];
1400+
start_generic_module_thread(
1401+
"sh".into(),
1402+
"/bin/sh".into(),
1403+
Some(args),
1404+
5600,
1405+
tx.clone(),
1406+
);
1407+
};
1408+
const QUICK: usize = 40;
1409+
for _ in 0..QUICK {
1410+
sh("sleep 20; : aw-tauri-spawn-test", &tx);
1411+
sh("exit 0", &tx);
1412+
}
1413+
let deadline = Instant::now() + Duration::from_secs(5);
1414+
let mut stopped = 0;
1415+
while stopped < QUICK {
1416+
match rx.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
1417+
Ok(ModuleMessage::Stopped { .. }) => stopped += 1,
1418+
Ok(_) => {}
1419+
Err(_) => break,
1420+
}
1421+
}
1422+
// Clean up the sleepers.
1423+
let _ = Command::new("pkill")
1424+
.args(["-f", "aw-tauri-spawn-test"])
1425+
.status();
1426+
assert_eq!(
1427+
stopped, QUICK,
1428+
"some short-lived modules never reported Stopped"
1429+
);
1430+
}
1431+
13801432
#[cfg(unix)]
13811433
#[test]
13821434
fn generic_module_captures_stderr() {

0 commit comments

Comments
 (0)