Give the version probe a deadline, so a hung binary cannot hang the tool - #11
Merged
Conversation
`install::read_version` was the one subprocess in this crate not behind the `Spawner` trait, and it ran with no deadline at all. A hung Factorio hung every command that picks an install: `installs list`, `run`, `provenance report`, `refs docs` and `refs sync`. No signature change. `None` already meant "this binary will not tell us its version", so a timeout is one more way to reach an answer every caller already handled, and `discover`, `select` and the callers below them needed no edit. No new dependency either; the crate holds at five. A reader thread drains stdout while the main thread watches the clock. That split is the point: the `Child` stays where `kill` can reach it, and the pipe is drained continuously, so a child that filled it is never misread as a hang. On timeout the child is killed and reaped, because `kill` alone leaves a zombie. The 10 second number is measured, not guessed. 15 runs on each of two installs on 2026-08-18: `factorio --version` took a median of 47.6 ms on 2.1.14 and 43.9 ms on 2.0.77, all output on stdout and zero bytes on stderr. The worst case is two deadlines, not one, because the poll loop and the reader each get the full time and they are additive when a grandchild holds the write end. Measured at 3.18 seconds with a 3 second grandchild. Two tests pin that, one per property, since a single test cannot fail on both. Six tests added, all `#[cfg(unix)]` because Windows `CreateProcess` runs neither a shebang script nor a `.cmd`. Every one was checked by mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FpiiN19F5Yfjc6jn4y7ttW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
install::read_versionwas the one subprocess in this crate not behind theSpawnertrait, and it ran with no deadline at all. A hung Factorio hung every command that picks an install:installs list,run,provenance report,refs docsandrefs sync. It also meantrefs sync --check, documented as "report only, never fetches, never writes", could hang forever.What changed
No signature change, and no new dependency.
Nonealready meant "this binary will not tell us its version", so a timeout is one more way to reach an answer every caller already handled.discover,selectand everything below them needed no edit. The crate holds at five dependencies.A reader thread drains stdout while the main thread watches the clock. That split is the point:
Childstays on the main thread, sokillis reachable. Handing the whole child to a thread reads more simply but leaves nothing able to stop the process.killalone leaves a zombie, because Rust'sChilddoes not reap on drop.The numbers are measured
15 runs on each of two installs, 2026-08-18.
factorio --versiontook a median of 47.6 ms on the 2.1.14 Steam install (min 44.9, max 54.9) and 43.9 ms on the 2.0.77 standalone (min 41.7, max 44.6). Each printed 116 bytes or fewer, all on stdout and zero on stderr, which is this repo's long-standing stderr finding measured a fourth time. Ten seconds is about 200 times the median.The worst case is two deadlines, not one
The poll loop and the reader each get the full time, and they are additive whenever the child exits while something else still holds the write end of stdout. Measured at 3.18 seconds with a 3 second grandchild, returning the right version.
Waiting is the intended behaviour. Returning early would drop output the child had already written, which is the failure the reader thread exists to prevent. Two tests pin it, one per property, because a single test cannot fail on both: one grandchild shorter than the deadline checks the version still parses, one longer checks the wait is bounded.
CLAUDE.mdrecords the 20 second worst case.Tests
Six added, all
#[cfg(unix)], because WindowsCreateProcessruns neither a shebang script nor a.cmd, and building a hanging binary there needs a second cargo target or a sixth dependency. CI is ubuntu-latest, so all six run on every push. The timeout is untested on Windows.Every one was checked by mutation, not by reading. Removing the kill, removing the reap, ignoring the deadline, forcing a 1 ms deadline, making the second wait unbounded, and making it return immediately each failed the test that claims to cover it.
One trap measured while debugging and recorded in a comment: a script file written seconds ago takes 345 to 438 ms on its first exec on macOS while the suite is spawning, so a 200 ms deadline killed the shell before its first line ran.
cargo fmt,cargo clippy -D warningsandcargo test --all-targetsall clean: 253 unit and 13 integration, 266 green, up from 260. The sharedfactorio-dataclone is unchanged, stillmasterata784954.🤖 Generated with Claude Code
https://claude.ai/code/session_01FpiiN19F5Yfjc6jn4y7ttW