The same class as #2101 / #2103 / #2105 / #2107 — an open is not a read. On a FIFO the open waits for a peer and raises nothing, so no except OSError around the call can fire: the call simply never returns.
The sites (emrg/client/daemon_manager.py, master 60541e39)
| site |
direction |
what a FIFO at the path does |
_truncate_start_stderr (:161, open(path, "wb")) |
writer |
start_daemon opens the child's stderr before it spawns, so the open waits for a reader and every start attempt from this client stops there |
_read_start_stderr (:185, path.read_bytes()) |
reader |
waits for a writer while the failure report is being built |
_read_log_tail (:241, path.exists() + open(path, "rb")) |
reader |
.exists() is true for a FIFO, which is exactly the guard that lets the block through |
The subjects are logs_dir()/emrgd-start.err and logs_dir()/emrgd.log (_start_stderr_path, _log_path).
Measured on master 60541e39
One child process per arm under an 8 s cap, with a regular-file control for each subject, so the axis read is the kind of the path and not its absence:
truncate regular -> RETURNED BufferedWriter
truncate fifo -> DID NOT RETURN within 8s
truncate directory -> RETURNED NoneType
read_start regular -> RETURNED ''
read_start fifo -> DID NOT RETURN within 8s
read_start directory -> RETURNED None
log_tail regular -> RETURNED ''
log_tail fifo -> DID NOT RETURN within 8s
log_tail directory -> RETURNED ''
The directory column is what fixes the design: the kind one over already produces each site's answer (None for the writer, which the caller reads as "fall back to DEVNULL"; None for the stderr reader, which the report already names "could not be read"; "" for the log tail, "nothing new"). So the guard adds no new outcome — only that the existing one arrives.
The fix
Ask the subject's kind before opening it, through the tool layer's path-taking non_regular_kind — imported rather than restated, so the boundary stays one fact with one spelling — and take the answer each site already gives. _read_log_tail's exists() is replaced rather than kept as a second opinion.
Acceptance
- each site returns its own existing answer for a non-regular subject instead of blocking;
- a FIFO-free arm per site, on a directory, that discriminates: both trees answer the same value there, so the arm asserts the kind is named in the log with the path — which the base cannot produce, and which is the arm the Windows leg runs;
- the regular-file controls are unchanged, and the FIFO arms are POSIX-gated (
os.mkfifo) and bounded by tests/bounded_read.within;
uv run pytest tests/ green, with check-undefined-names, check-doc-count, check-citation-resolves, check_nonlocal and check_unbound_reads at rc 0.
Out of scope, named rather than silently under-reported: the GUI's JS client carries the same three shapes (emrg/gui/daemon_client.js:247, :262, :278 — readFileSync / openSync(file, "w") / readFileSync). This issue does not cover it.
The same class as #2101 / #2103 / #2105 / #2107 — an open is not a read. On a FIFO the open waits for a peer and raises nothing, so no
except OSErroraround the call can fire: the call simply never returns.The sites (
emrg/client/daemon_manager.py, master60541e39)_truncate_start_stderr(:161,open(path, "wb"))start_daemonopens the child's stderr before it spawns, so the open waits for a reader and every start attempt from this client stops there_read_start_stderr(:185,path.read_bytes())_read_log_tail(:241,path.exists()+open(path, "rb")).exists()is true for a FIFO, which is exactly the guard that lets the block throughThe subjects are
logs_dir()/emrgd-start.errandlogs_dir()/emrgd.log(_start_stderr_path,_log_path).Measured on master
60541e39One child process per arm under an 8 s cap, with a regular-file control for each subject, so the axis read is the kind of the path and not its absence:
The directory column is what fixes the design: the kind one over already produces each site's answer (
Nonefor the writer, which the caller reads as "fall back toDEVNULL";Nonefor the stderr reader, which the report already names "could not be read";""for the log tail, "nothing new"). So the guard adds no new outcome — only that the existing one arrives.The fix
Ask the subject's kind before opening it, through the tool layer's path-taking
non_regular_kind— imported rather than restated, so the boundary stays one fact with one spelling — and take the answer each site already gives._read_log_tail'sexists()is replaced rather than kept as a second opinion.Acceptance
os.mkfifo) and bounded bytests/bounded_read.within;uv run pytest tests/green, withcheck-undefined-names,check-doc-count,check-citation-resolves,check_nonlocalandcheck_unbound_readsat rc 0.Out of scope, named rather than silently under-reported: the GUI's JS client carries the same three shapes (
emrg/gui/daemon_client.js:247,:262,:278—readFileSync/openSync(file, "w")/readFileSync). This issue does not cover it.