Line references at rewrite b58160b, and at ab-av1 upstream/main 0e682f2 for the dependency. Same class as #82, different code path: #82 covers the media.rs probes and assumes "force-stop reaches the ab-av1/remux child". It does not reach this one.
What happens
run_search and run_encode each probe the input synchronously before entering their cancellation loop:
ab_av1/operation.rs:80 — let probe = Arc::new(ffprobe::probe(&request.input));, with the tokio::select! cancellation loop only starting at :89.
ab_av1/operation.rs:159 — same, loop starts at :163.
ffprobe::probe is ab_av1::ffprobe::probe (ffprobe.rs:42 upstream), which calls the ffprobe 0.4 crate. That crate uses std::process::Command::output(): a real subprocess, blocking the calling thread until it exits, with no timeout. Upstream has nine such synchronous call sites in async contexts, including command/sample_encode.rs:292 inside the sample loop.
1. It blocks the whole runtime
The adapter runs on Builder::new_current_thread() on a dedicated OS thread (ab_av1/runtime.rs:412). A blocking call there stops every task, including the one that would observe a cancellation. The pre-loop cancellation checks at :77 and :156 have already passed by then, so a cancel arriving during the probe is not seen until the probe returns.
Upstream has the same exposure in its own CLI (main.rs:32 is #[tokio::main(flavor = "current_thread")]), and worse: at the first probe of each command the block precedes any poll of signal::ctrl_c(), so the handler may never install.
2. cancel_job cannot reach the child
The ffprobe crate spawns through std::process::Command, not through process::child::spawn, so the child gets no dedicated process group and no CREATE_NO_WINDOW, and is never registered in ab-av1's child registry. cancel_job() (operation.rs:205) iterates that registry, so it cannot terminate this probe.
To be precise about a thing that is easy to overstate: std::process::Command does not setsid, so the child is not group-less, it inherits ours. It is unreachable by our per-job kill, not an orphan in the general sense.
When this actually bites
Verified against upstream with a hanging ffprobe shim. At an interactive TTY a hung ffprobe is killed by Ctrl-C, because the signal reaches the whole foreground process group, and the probe unblocks. Reproducing the wedge required trap '' INT TERM in the shim plus a kill -INT targeted at the ab-av1 pid alone. Reproduction showed the process still alive after two SIGINTs; only SIGTERM ended it, bypassing cleanup entirely. A control run blocked on an async ffmpeg child instead exited in about a second with Error: ctrl_c.
That leaves two live cases, and both are ours:
- ffprobe stuck in uninterruptible D-state I/O (network mount, failing disk), where signals do not help.
- A signal delivered programmatically to the supervised process alone, which is exactly what a GUI supervisor does.
In either case the job worker wedges with no path to recovery: cancellation is not observed, force-stop does not reach the process, and wait_for_report polls indefinitely (ab_av1/job.rs:518-553).
Direction
The bounded mechanism already exists in media.rs:215-270 (probe_supervised, 30s timeout, cooperative cancel), as #82 observes. The adapter can probe through it and hand the result over, because crf_search::run and encode::run both take a caller-supplied Arc<Ffprobe> rather than probing themselves. That fixes operation.rs:80 and :159 entirely.
It does not fix sample_encode.rs:292, which is inside the dependency. That one needs an upstream change (make probe async with the body in spawn_blocking, roughly 30 lines across the nine call sites) or a patch in our pinned fork.
Line references at
rewriteb58160b, and at ab-av1upstream/main0e682f2 for the dependency. Same class as #82, different code path: #82 covers themedia.rsprobes and assumes "force-stop reaches the ab-av1/remux child". It does not reach this one.What happens
run_searchandrun_encodeeach probe the input synchronously before entering their cancellation loop:ab_av1/operation.rs:80—let probe = Arc::new(ffprobe::probe(&request.input));, with thetokio::select!cancellation loop only starting at:89.ab_av1/operation.rs:159— same, loop starts at:163.ffprobe::probeisab_av1::ffprobe::probe(ffprobe.rs:42upstream), which calls theffprobe0.4 crate. That crate usesstd::process::Command::output(): a real subprocess, blocking the calling thread until it exits, with no timeout. Upstream has nine such synchronous call sites in async contexts, includingcommand/sample_encode.rs:292inside the sample loop.1. It blocks the whole runtime
The adapter runs on
Builder::new_current_thread()on a dedicated OS thread (ab_av1/runtime.rs:412). A blocking call there stops every task, including the one that would observe a cancellation. The pre-loop cancellation checks at:77and:156have already passed by then, so a cancel arriving during the probe is not seen until the probe returns.Upstream has the same exposure in its own CLI (
main.rs:32is#[tokio::main(flavor = "current_thread")]), and worse: at the first probe of each command the block precedes any poll ofsignal::ctrl_c(), so the handler may never install.2.
cancel_jobcannot reach the childThe
ffprobecrate spawns throughstd::process::Command, not throughprocess::child::spawn, so the child gets no dedicated process group and noCREATE_NO_WINDOW, and is never registered in ab-av1's child registry.cancel_job()(operation.rs:205) iterates that registry, so it cannot terminate this probe.To be precise about a thing that is easy to overstate:
std::process::Commanddoes notsetsid, so the child is not group-less, it inherits ours. It is unreachable by our per-job kill, not an orphan in the general sense.When this actually bites
Verified against upstream with a hanging ffprobe shim. At an interactive TTY a hung ffprobe is killed by Ctrl-C, because the signal reaches the whole foreground process group, and the probe unblocks. Reproducing the wedge required
trap '' INT TERMin the shim plus akill -INTtargeted at the ab-av1 pid alone. Reproduction showed the process still alive after two SIGINTs; only SIGTERM ended it, bypassing cleanup entirely. A control run blocked on an async ffmpeg child instead exited in about a second withError: ctrl_c.That leaves two live cases, and both are ours:
In either case the job worker wedges with no path to recovery: cancellation is not observed, force-stop does not reach the process, and
wait_for_reportpolls indefinitely (ab_av1/job.rs:518-553).Direction
The bounded mechanism already exists in
media.rs:215-270(probe_supervised, 30s timeout, cooperative cancel), as #82 observes. The adapter can probe through it and hand the result over, becausecrf_search::runandencode::runboth take a caller-suppliedArc<Ffprobe>rather than probing themselves. That fixesoperation.rs:80and:159entirely.It does not fix
sample_encode.rs:292, which is inside the dependency. That one needs an upstream change (makeprobeasync with the body inspawn_blocking, roughly 30 lines across the nine call sites) or a patch in our pinned fork.