29 issues across PTY (lib/src/pty/), IPC (lib/src/ipc/), and daemon command handlers (lib/src/daemon/). 14 critical (silent failures, resource leaks, races), 8 high (degraded UX/debug), 7 medium (cleanliness). Each item references the follow-up ticket where the fix lands (T-75 through T-81). Co-Authored-By: Claude <noreply@anthropic.com>
7.3 KiB
PTY + IPC error-handling audit
Date: 2026-05-05 Ticket: T-18 Decision ref: D-5
Punch list of error-handling issues in lib/src/pty/, lib/src/ipc/,
and lib/src/daemon/. Severity-ranked. Each item references the
follow-up ticket where the fix lands.
Critical — silent failures, leaks, races
-
lib/src/pty/native_pty.dart:155-158—forkpty()failure throwsStateError('forkpty() failed')with no errno. Caller can't distinguish ENOMEM/EAGAIN/ENOENT-of-/dev/ptmx. Capture errno before_freeAll(which may trample it) and surface viaPtyException. → T-75 -
lib/src/pty/native_pty.dart:160-165— Child process:chdirandexecvereturns are ignored. Ifexecvereturns (i.e. fails), we fall through to_exit(1)with no diagnostic. Write a one-line error envelope to fd 1 before exiting so the parent's reader sees "exec failed: ENOENT" instead of immediate EOF. → T-75 -
lib/src/pty/native_pty.dart:244-251—write()ignores_nativeWritereturn. Short writes silently drop bytes; -1/EPIPE reported as successful "wrote -1". Loop until full length is written or surface errno on negative returns. → T-75 -
lib/src/pty/native_pty.dart:259-262—resize()ignores_ioctland_nativeKillreturn values. EBADF on a half-closed fd silently no-ops. Set_dead = trueon EBADF. → T-75 -
lib/src/pty/native_pty.dart:198-210— Race:_spawnReaderisasyncbutNativePty.startreturns immediately.close()racing with isolate spawn can leave the isolate orphaned. Makestartawait reader spawn or track the spawn-future. → T-76 -
lib/src/pty/native_pty.dart:280-290—close()sets_dead = truebefore_nativeClose(_fd), but the reader isolate continues polling on_fd. If a new fd reuses that number, the reader'spollmay briefly target the wrong file. Send shutdown signal via SendPort or self-pipe before closing. → T-76 -
lib/src/pty/session.dart:135-153— Resource leak: if_recvFdAsync,setWinsize,proc.stdout.first.timeout, or_extractPidthrows, the spawned ptyc Process and (in some cases) the receivedmasterFdleak. Only line 151 closesmasterFd. Wrap post-spawn block in try/catch that killsproc, closesmasterFd, and rethrows. → T-76 -
lib/src/pty/session.dart:240—_recvFdAsync: ifIsolate.spawnitself throws,portis leaked. Wrap in try/catch. → T-76 -
lib/src/pty/session.dart:165-176—write()returns rawlibc.writeresult without checking < 0 / errno or looping for short writes. Same as #3. → T-75 -
lib/src/pty/session.dart:271-275—Isolate.spawn(...).then(...)is fire-and-forget. If spawn fails, the error is silently swallowed and_readerIsolateremains null forever. Add.catchErroror await it. → T-76 -
lib/src/ipc/server.dart:30-39—broadcast()try/catch (_)swallows write errors with no logging. At least log the kind. → T-77 -
lib/src/ipc/server.dart:107—client.writeln(resp.encode())is not awaited and not guarded. If client disconnected mid-dispatch, this throws asynchronously with noonErrorhandler. Wrap in try/catch and remove the client from_clients. → T-77 -
lib/src/ipc/server.dart:83-108—_handleLinerunsawait dispatch(msg)with no per-request timeout. A misbehaving handler blocks the connection's read pipeline indefinitely. → T-77 -
lib/src/ipc/server.dart:46-50— Stale-socket retry deletes the socket file unconditionally onSocketException. If two daemon instances race to start, the second rips the first's live socket out from under it. Tryconnect()first; refuse if a live daemon answers. → T-77
High — degraded UX / debugging
-
lib/src/daemon/pane_commands.dart:87-96—_spawncatch-all flattens every failure intotool_error: pane.spawn failed: <toString>. "binary not found", "permission denied", "out of pty fds" all look the same. MapPtyException.errno(ENOENT/EACCES/EMFILE) to distinct hints/codes. → T-79 -
lib/src/daemon/editor_commands.dart:67-76— Same pattern;editor.opencatch-all loses FileSystemException distinctions (ENOENT vs EACCES vs EISDIR). → T-79 -
lib/src/daemon/files_commands.dart:78—file.readAsStringSync()is unguarded; UTF-8 errors, permission errors, races with deletion turn into a 500-style dispatch error instead of a cleanIpcResponse.err. Wrap in try/catch. → T-81 -
lib/src/daemon/files_commands.dart:74— Path is concatenated with/and never validated.path: "../../../etc/passwd"traverses out offiles.root. Resolve and verify the resulting path stays underroot.absolute.path. → T-78 (security) -
lib/src/pty/session.dart:201-234—close()distinguishes EOF/EBADF/EIO only in comments. The 500ms timeout is silent (onTimeout: () {}). Log the timeout so we know when SIGKILL was actually needed. → T-81 -
lib/src/pty/session.dart:390-394— Reader isolate treats any negative read return that isn't EINTR as EOF — including transient EAGAIN or recoverable EIO. Inspect errno and log non-EBADF/EIO/0 cases. → T-81 -
lib/src/pty/ffi/scm_rights.dart:115-116— Returned cmsg-data fd is read without sanity-checking againstmsgControllen. A malformed peer that sends only a partial cmsg could let us read garbage as an fd. VerifydataOffset + 4 <= msgControllenbefore deref. → T-81 -
lib/src/ipc/server.dart:41-56—start()logs tostderr.writelnbut the rest of the daemon uses no logger. In the Flutter-host process stderr is often consumed by the engine. Standardize on a logger. → T-80
Medium — cleanliness
-
lib/src/pty/session.dart:390,native_pty.dart:262, 285— Magic errno/signal numbers (4=EINTR,9=SIGKILL,28=SIGWINCH,_kSighup=1). Pull into named constants. → T-80 -
lib/src/pty/ffi/libc.dart:232-245—errnogetter does alookupFunctionon every access (catching ArgumentError every call on macOS). Cache the resolved function pointer. → T-80 -
lib/src/daemon/git_commands.dart:283—_gitErroralways reportstool_error. Agit pushrejection or merge conflict is user-actionable, not a tool failure; could map toIpcExitCode.conflictwhen stderr matches known patterns. → T-81 -
lib/src/ipc/server.dart:97— Dispatch error showsdispatch failed: $e(full exception toString). Trim and add the requestcmdfor log correlation. → T-80 -
lib/src/daemon/pane_commands.dart:136—registry.write(id, bytes)return valuenis shown to caller, but ifn == -1(write failed) we still respondok. Distinguish. → T-81 -
lib/src/ipc/envelope.dart:88-94—IpcResponse.fromJsonthrowsTypeErrorifok=falsebuterroris missing. No graceful degradation for a malformed peer response. → T-81 -
lib/src/pty/native_pty.dart:111-119— PATH resolution silently uses the first existing match without checkingX_OK. A non-executable file shadows a valid binary further along PATH. → T-81