-
Notifications
You must be signed in to change notification settings - Fork 1
fix: compact/goal lock-wait unit bug + backend stdin error handling #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,12 +15,12 @@ | |||||||||||||||||||
| * workers) with `process.kill(-pid)` and leave no orphans. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { spawn, type ChildProcess } from "node:child_process"; | ||||||||||||||||||||
| import { createInterface } from "node:readline"; | ||||||||||||||||||||
| import process from "node:process"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { log } from "../utils.js"; | ||||||||||||||||||||
| import type { | ||||||||||||||||||||
| ZcodeEvent, | ||||||||||||||||||||
| ZcodeInbound, | ||||||||||||||||||||
| ZcodeInteractionPermissionParams, | ||||||||||||||||||||
|
|
@@ -65,6 +65,14 @@ | |||||||||||||||||||
| env, | ||||||||||||||||||||
| detached: true, // own process group → kill(-pid) reaps the whole tree | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| // Node stream write errors (EPIPE on a closed stdin) are emitted as async | ||||||||||||||||||||
| // 'error' events, NOT thrown synchronously — without a listener the process | ||||||||||||||||||||
| // crashes with an unhandled 'error' event. Catch them here and mark the | ||||||||||||||||||||
| // reader dead so the rest of the bridge stops talking to a gone backend. | ||||||||||||||||||||
| this.proc.stdin?.on("error", (err) => { | ||||||||||||||||||||
| this.readerDead = true; | ||||||||||||||||||||
| log(`backend: stdin error: ${err.message}`); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| this.startReader(); | ||||||||||||||||||||
| this.startWatchdog(); | ||||||||||||||||||||
| log(`backend: started zcode app-server (pid=${this.proc.pid})`); | ||||||||||||||||||||
|
|
@@ -216,17 +224,10 @@ | |||||||||||||||||||
| log("backend: sendReply dropped (stdin closed)"); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| stdin.write(JSON.stringify({ id, result }) + "\n"); | ||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||
| // Broken pipe: backend is gone. Mark reader dead so the rest of the | ||||||||||||||||||||
| // bridge stops trying to talk to it; otherwise zcode reannounces would | ||||||||||||||||||||
| // keep hitting a dead pipe and the dedup cache would spin forever. | ||||||||||||||||||||
| this.readerDead = true; | ||||||||||||||||||||
| log( | ||||||||||||||||||||
| `backend: sendReply write failed (marked dead): ${e instanceof Error ? e.message : String(e)}`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // Write errors (EPIPE) are delivered via the stdin 'error' listener | ||||||||||||||||||||
| // installed in the constructor (synchronous try/catch cannot catch them); | ||||||||||||||||||||
| // no try/catch needed here. | ||||||||||||||||||||
| stdin.write(JSON.stringify({ id, result }) + "\n"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** Reply to a zcode server→client request with an error. */ | ||||||||||||||||||||
|
|
@@ -236,14 +237,7 @@ | |||||||||||||||||||
| log("backend: sendError dropped (stdin closed)"); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| stdin.write(JSON.stringify({ id, error: { code, message } }) + "\n"); | ||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||
| this.readerDead = true; | ||||||||||||||||||||
| log( | ||||||||||||||||||||
| `backend: sendError write failed (marked dead): ${e instanceof Error ? e.message : String(e)}`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| stdin.write(JSON.stringify({ id, error: { code, message } }) + "\n"); | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to
Suggested change
|
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ---------- send / request ---------- | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it is true that asynchronous write errors (like
EPIPE) are emitted as'error'events on the stream and handled by the new listener in the constructor,stdin.write()can still throw synchronous exceptions (such asERR_STREAM_DESTROYEDorERR_STREAM_WRITE_AFTER_END) if the stream has already been closed, ended, or destroyed butstdin.destroyedis not yet updated. Without atry/catchblock, these synchronous exceptions will go uncaught and crash the entire Node.js process. Keeping the defensivetry/catchblock aroundstdin.write()is highly recommended for robustness.