fix(command): no timer leak on spawn error + UTF-8 chunk reassembly#50
Open
Asgarrrr wants to merge 1 commit into
Open
fix(command): no timer leak on spawn error + UTF-8 chunk reassembly#50Asgarrrr wants to merge 1 commit into
Asgarrrr wants to merge 1 commit into
Conversation
Two correctness fixes in `CommandObserver.observe`:
- Switch the timeout cleanup from a post-await `clearTimeout(timer)` to
`await closed.finally(() => clearTimeout(timer))`. Without it, a
`proc.error` event (bad cwd, EMFILE, fork failure) rejected the
`closed` promise without ever clearing the timer — leaving the
default 300s timeout armed on the event loop after observe()
rejected, and potentially firing a SIGKILL on a stale pid later.
- Add `setEncoding("utf8")` on stdout/stderr streams. Decoding each
Buffer chunk independently with `chunk.toString("utf8")` corrupts
multibyte codepoints split across two `data` events into pairs of
replacement-character fragments. `setEncoding` routes chunks through
Node's StringDecoder, which buffers partial codepoints across emits.
Adds one test asserting observe() rejects on an invalid cwd, documenting
the error-path contract that the timer-leak fix relies on.
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.
Summary
Two correctness fixes in
CommandObserver.observe(src/command/command-observer.ts). Both are tiny — total +19/-6 in production code, +12 lines of test.What changed
await closed; clearTimeout(timer)left the timer armed ifclosedrejected (bad cwd, EMFILE, fork failure) — default 300s timer kept the event loop alive afterobserve()rejected, and could fire a SIGKILL on a stale pid later.await closed.finally(() => clearTimeout(timer)).datahandlers was decoded withchunk.toString("utf8")independently, so a multibyte UTF-8 codepoint split across two emits became two replacement-char fragments — non-ASCII output corruption (emoji, CJK, accented French/EU text) under stream pressure.proc.stdout?.setEncoding("utf8")/proc.stderr?.setEncoding("utf8")so chunks flow through Node'sStringDecoder, which buffers partial codepoints across emits.Why now
The H1 audit (orphan-process timeout) hardened the kill path; this PR closes two adjacent correctness holes on the same code path:
proc.errorevent still leaks the timer because the rejection skips the post-awaitclearTimeout.Promise.finallymakes it symmetric.tsc,bun test) emitting >64KB-ish output, chunks fragment at arbitrary byte boundaries. ASCII tests didn't surface the issue because every codepoint is one byte.Neither bug had a reported user incident, but both are deterministic correctness gaps under load and on non-ASCII output.
Test plan
bun test tests/command-observer.test.ts— 14 pass / 0 fail (13 existing + 1 new bad-cwd test)bun test— 317 pass / 0 fail / 23 filesobserver.observe("echo hi", "<missing-dir>")rejects rather than hangs (new test)Risk / rollback
setEncodingswitches the stream from object-mode-Buffer to object-mode-string. Any future code readingproc.stdoutdirectly would receive strings instead of Buffers. Today there is no such consumer; the only reader is thedatahandler in this file.git revert.Out of scope (deferred)
stdoutBuf/stderrBuf/combinedBufgrow without limit; a multi-hundred-MB output command can OOM the host. Real concern, but the fix (streaming-to-disk + capped in-memory tail for the returnedObserveResult) is a bigger semantic change than warranted here.catchmasks permission errors. Low impact, separate PR if at all.