What happens
child_output_with_timeout in desktop/src-tauri/src/main.rs is supposed to
give up on a wedged subprocess after a fixed timeout. It does not, when the
child spawned children of its own. It waits for as long as the grandchild lives,
whatever the timeout said.
a_gpu_probe_that_never_returns_is_given_up_on catches this. It asks for a
2 second timeout and asserts the call returns inside 30 seconds. It takes 300.
The Linux Rust Check on #569 has been failing on it since 8dc1577.
Why
The timeout branch kills the child and then joins both reader threads:
let _ = child.kill();
let _ = child.wait();
// Joined rather than detached: killing the child closes its ends,
// so the readers finish, and dropping the handles without joining
// would leak two threads per timeout.
let _ = collect(stdout_reader);
let _ = collect(stderr_reader);
The comment is wrong. Killing the child closes only the handles the child
itself held. A grandchild inherited the same pipe write ends, so the pipe stays
open and read_to_end in the reader thread keeps blocking. Joining it is
waiting for the grandchild.
The test's stand-in makes this concrete: sh -c "sleep 300" forks on Ubuntu
rather than exec-ing, so there really are two processes. Killing sh leaves
sleep holding the stderr pipe for its full 300 seconds, and the join waits it
out.
Why it matters beyond the test
This is the same failure #502 is about, one level down. run_pip_install uses
this path with a 20 minute timeout, so a wedged pip whose child outlives it
would hang setup rather than being given up on, with the timeout offering no
protection at all. The bound exists precisely so setup cannot stop forever.
Constraints
- The timeout has to be hard. Nothing on that path may wait on something whose
lifetime we do not control.
- Not joining is not a leak worth worrying about: the reader thread ends by
itself when the last writer closes the pipe. That is the same instant the
join would have returned, minus the part where it blocks the caller.
- The non-timeout path must keep joining. There the child has exited, the reader
is at EOF, and the collected output is the return value.
- Killing the whole process group so grandchildren die too is a separate and
larger change (process_group plus killpg on Unix, a Job Object on
Windows). None of the real callers here spawn grandchildren -- the GPU probe
is a bare python -c, and pip installs wheels without build subprocesses --
so it is worth doing on its own terms rather than folded into this.
What happens
child_output_with_timeoutindesktop/src-tauri/src/main.rsis supposed togive up on a wedged subprocess after a fixed timeout. It does not, when the
child spawned children of its own. It waits for as long as the grandchild lives,
whatever the timeout said.
a_gpu_probe_that_never_returns_is_given_up_oncatches this. It asks for a2 second timeout and asserts the call returns inside 30 seconds. It takes 300.
The Linux Rust Check on #569 has been failing on it since 8dc1577.
Why
The timeout branch kills the child and then joins both reader threads:
The comment is wrong. Killing the child closes only the handles the child
itself held. A grandchild inherited the same pipe write ends, so the pipe stays
open and
read_to_endin the reader thread keeps blocking. Joining it iswaiting for the grandchild.
The test's stand-in makes this concrete:
sh -c "sleep 300"forks on Ubunturather than exec-ing, so there really are two processes. Killing
shleavessleepholding the stderr pipe for its full 300 seconds, and the join waits itout.
Why it matters beyond the test
This is the same failure #502 is about, one level down.
run_pip_installusesthis path with a 20 minute timeout, so a wedged pip whose child outlives it
would hang setup rather than being given up on, with the timeout offering no
protection at all. The bound exists precisely so setup cannot stop forever.
Constraints
lifetime we do not control.
itself when the last writer closes the pipe. That is the same instant the
join would have returned, minus the part where it blocks the caller.
is at EOF, and the collected output is the return value.
larger change (
process_grouppluskillpgon Unix, a Job Object onWindows). None of the real callers here spawn grandchildren -- the GPU probe
is a bare
python -c, and pip installs wheels without build subprocesses --so it is worth doing on its own terms rather than folded into this.