PTY: fix resource leaks and reader-isolate races (T-76)

NativePty.close() now awaits the reader-isolate spawn, kills the
child first to drive EOF on the master fd, awaits the isolate's
EOF acknowledgement, and only then closes the fd. Previously the
fd-close racing with the polling isolate left a window where the
fd number could be reused and the isolate would briefly target the
wrong file.

Both NativePty and PtySession now surface reader-isolate spawn
errors via the output stream's addError instead of silently
swallowing them.

PtySession.spawn closes the master fd on any post-receive failure,
closes parentSock in finally (was leaking on every spawn), and
kills the ptyc process if recvFd fails.

PtySession._recvFdAsync uses try/finally to close the ReceivePort
and kill the spawn isolate even when Isolate.spawn itself throws.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-05 15:10:43 +02:00
co-authored by Claude
parent 0d333e8470
commit 1787147e82
4 changed files with 244 additions and 139 deletions
+124 -110
View File
@@ -1,5 +1,5 @@
{ {
"exported_at": "2026-05-05T13:07:25Z", "exported_at": "2026-05-05T13:10:43Z",
"decisions": [ "decisions": [
{ {
"id": "D-1", "id": "D-1",
@@ -9,7 +9,7 @@
"status": "active", "status": "active",
"date": "2026-04-20", "date": "2026-04-20",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-3", "id": "D-3",
@@ -19,7 +19,7 @@
"status": "active", "status": "active",
"date": "2026-04-20", "date": "2026-04-20",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-4", "id": "D-4",
@@ -29,7 +29,7 @@
"status": "active", "status": "active",
"date": "2026-04-20", "date": "2026-04-20",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-5", "id": "D-5",
@@ -39,7 +39,7 @@
"status": "active", "status": "active",
"date": "2026-04-20", "date": "2026-04-20",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-6", "id": "D-6",
@@ -49,7 +49,7 @@
"status": "active", "status": "active",
"date": "2026-04-20", "date": "2026-04-20",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-7", "id": "D-7",
@@ -59,7 +59,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-8", "id": "D-8",
@@ -69,7 +69,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-9", "id": "D-9",
@@ -79,7 +79,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-10", "id": "D-10",
@@ -89,7 +89,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-11", "id": "D-11",
@@ -99,7 +99,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-12", "id": "D-12",
@@ -109,7 +109,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-13", "id": "D-13",
@@ -119,7 +119,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-14", "id": "D-14",
@@ -129,7 +129,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-15", "id": "D-15",
@@ -139,7 +139,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/extensions.md", "file_path": "decisions/extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-16", "id": "D-16",
@@ -149,7 +149,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/extensions.md", "file_path": "decisions/extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-17", "id": "D-17",
@@ -159,7 +159,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/extensions.md", "file_path": "decisions/extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-18", "id": "D-18",
@@ -169,7 +169,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/extensions.md", "file_path": "decisions/extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-19", "id": "D-19",
@@ -179,7 +179,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/extensions.md", "file_path": "decisions/extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-20", "id": "D-20",
@@ -189,7 +189,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/accessibility.md", "file_path": "decisions/accessibility.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-21", "id": "D-21",
@@ -199,7 +199,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/accessibility.md", "file_path": "decisions/accessibility.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-22", "id": "D-22",
@@ -209,7 +209,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/accessibility.md", "file_path": "decisions/accessibility.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-23", "id": "D-23",
@@ -219,7 +219,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-24", "id": "D-24",
@@ -229,7 +229,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-25", "id": "D-25",
@@ -239,7 +239,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-26", "id": "D-26",
@@ -249,7 +249,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-27", "id": "D-27",
@@ -259,7 +259,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-28", "id": "D-28",
@@ -269,7 +269,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-29", "id": "D-29",
@@ -279,7 +279,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-30", "id": "D-30",
@@ -289,7 +289,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/testing.md", "file_path": "decisions/testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-31", "id": "D-31",
@@ -299,7 +299,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-32", "id": "D-32",
@@ -309,7 +309,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-33", "id": "D-33",
@@ -319,7 +319,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-34", "id": "D-34",
@@ -329,7 +329,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-35", "id": "D-35",
@@ -339,7 +339,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-36", "id": "D-36",
@@ -349,7 +349,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-37", "id": "D-37",
@@ -359,7 +359,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-38", "id": "D-38",
@@ -369,7 +369,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-39", "id": "D-39",
@@ -379,7 +379,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-40", "id": "D-40",
@@ -389,7 +389,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/process.md", "file_path": "decisions/process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-41", "id": "D-41",
@@ -399,7 +399,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-42", "id": "D-42",
@@ -409,7 +409,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-43", "id": "D-43",
@@ -419,7 +419,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-44", "id": "D-44",
@@ -429,7 +429,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-45", "id": "D-45",
@@ -439,7 +439,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-46", "id": "D-46",
@@ -449,7 +449,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/extensions.md", "file_path": "decisions/extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-47", "id": "D-47",
@@ -459,7 +459,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-48", "id": "D-48",
@@ -469,7 +469,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-49", "id": "D-49",
@@ -479,7 +479,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-50", "id": "D-50",
@@ -489,7 +489,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-51", "id": "D-51",
@@ -499,7 +499,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-52", "id": "D-52",
@@ -509,7 +509,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-53", "id": "D-53",
@@ -519,7 +519,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-54", "id": "D-54",
@@ -529,7 +529,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-55", "id": "D-55",
@@ -539,7 +539,7 @@
"status": "active", "status": "active",
"date": "2026-04-23", "date": "2026-04-23",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-56", "id": "D-56",
@@ -549,7 +549,7 @@
"status": "active", "status": "active",
"date": "2026-04-23", "date": "2026-04-23",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-57", "id": "D-57",
@@ -559,7 +559,7 @@
"status": "active", "status": "active",
"date": "2026-04-23", "date": "2026-04-23",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-58", "id": "D-58",
@@ -569,7 +569,7 @@
"status": "active", "status": "active",
"date": "2026-04-23", "date": "2026-04-23",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-59", "id": "D-59",
@@ -579,7 +579,7 @@
"status": "active", "status": "active",
"date": "2026-04-25", "date": "2026-04-25",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-60", "id": "D-60",
@@ -589,7 +589,7 @@
"status": "active", "status": "active",
"date": "2026-04-26", "date": "2026-04-26",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-61", "id": "D-61",
@@ -599,7 +599,7 @@
"status": "active", "status": "active",
"date": "2026-04-26", "date": "2026-04-26",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-62", "id": "D-62",
@@ -609,7 +609,7 @@
"status": "active", "status": "active",
"date": "2026-04-26", "date": "2026-04-26",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-63", "id": "D-63",
@@ -619,7 +619,7 @@
"status": "active", "status": "active",
"date": "2026-04-26", "date": "2026-04-26",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-64", "id": "D-64",
@@ -629,7 +629,7 @@
"status": "active", "status": "active",
"date": "2026-05-03", "date": "2026-05-03",
"file_path": "decisions/architecture.md", "file_path": "decisions/architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "D-65", "id": "D-65",
@@ -639,7 +639,7 @@
"status": "active", "status": "active",
"date": "2026-04-26", "date": "2026-04-26",
"file_path": "decisions/tooling.md", "file_path": "decisions/tooling.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-1", "id": "Q-1",
@@ -648,7 +648,7 @@
"title": "Authorisation granularity on the IPC socket", "title": "Authorisation granularity on the IPC socket",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-2", "id": "Q-2",
@@ -657,7 +657,7 @@
"title": "Back-pressure on event streams", "title": "Back-pressure on event streams",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-3", "id": "Q-3",
@@ -666,7 +666,7 @@
"title": "Event persistence + audit/undo", "title": "Event persistence + audit/undo",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-4", "id": "Q-4",
@@ -675,7 +675,7 @@
"title": "`.canvas` schema compatibility with Obsidian", "title": "`.canvas` schema compatibility with Obsidian",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-5", "id": "Q-5",
@@ -684,7 +684,7 @@
"title": "IPC wire-format stability + `schema_version:`", "title": "IPC wire-format stability + `schema_version:`",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-6", "id": "Q-6",
@@ -693,7 +693,7 @@
"title": "Window chrome — native frame vs frameless custom", "title": "Window chrome — native frame vs frameless custom",
"status": "resolved", "status": "resolved",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-7", "id": "Q-7",
@@ -702,7 +702,7 @@
"title": "macOS app bundle signing / notarisation", "title": "macOS app bundle signing / notarisation",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-8", "id": "Q-8",
@@ -711,7 +711,7 @@
"title": "Extension API shape — widgets, subcommands, both?", "title": "Extension API shape — widgets, subcommands, both?",
"status": "open", "status": "open",
"file_path": "decisions/questions-extensions.md", "file_path": "decisions/questions-extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-9", "id": "Q-9",
@@ -720,7 +720,7 @@
"title": "Lua runtime vendoring", "title": "Lua runtime vendoring",
"status": "open", "status": "open",
"file_path": "decisions/questions-extensions.md", "file_path": "decisions/questions-extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-10", "id": "Q-10",
@@ -729,7 +729,7 @@
"title": "Extension manifest `schema_version:`", "title": "Extension manifest `schema_version:`",
"status": "open", "status": "open",
"file_path": "decisions/questions-extensions.md", "file_path": "decisions/questions-extensions.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-11", "id": "Q-11",
@@ -738,7 +738,7 @@
"title": "Coverage gates — hard thresholds vs soft reporting", "title": "Coverage gates — hard thresholds vs soft reporting",
"status": "open", "status": "open",
"file_path": "decisions/questions-testing.md", "file_path": "decisions/questions-testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-12", "id": "Q-12",
@@ -747,7 +747,7 @@
"title": "Screen-reader automation (axe-core via Playwright)", "title": "Screen-reader automation (axe-core via Playwright)",
"status": "open", "status": "open",
"file_path": "decisions/questions-testing.md", "file_path": "decisions/questions-testing.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-13", "id": "Q-13",
@@ -756,7 +756,7 @@
"title": "Web production-mode a11y", "title": "Web production-mode a11y",
"status": "open", "status": "open",
"file_path": "decisions/questions-accessibility.md", "file_path": "decisions/questions-accessibility.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-14", "id": "Q-14",
@@ -765,7 +765,7 @@
"title": "i18n plurals / gender / date-format tooling", "title": "i18n plurals / gender / date-format tooling",
"status": "open", "status": "open",
"file_path": "decisions/questions-accessibility.md", "file_path": "decisions/questions-accessibility.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-15", "id": "Q-15",
@@ -774,7 +774,7 @@
"title": "Editor tab — full LSP vs tree-sitter-only highlight", "title": "Editor tab — full LSP vs tree-sitter-only highlight",
"status": "open", "status": "open",
"file_path": "decisions/questions-process.md", "file_path": "decisions/questions-process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-16", "id": "Q-16",
@@ -783,7 +783,7 @@
"title": "`tree-sitter-dart` grammar maintenance", "title": "`tree-sitter-dart` grammar maintenance",
"status": "open", "status": "open",
"file_path": "decisions/questions-process.md", "file_path": "decisions/questions-process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-17", "id": "Q-17",
@@ -792,7 +792,7 @@
"title": "Icon set growth", "title": "Icon set growth",
"status": "open", "status": "open",
"file_path": "decisions/questions-process.md", "file_path": "decisions/questions-process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-18", "id": "Q-18",
@@ -801,7 +801,7 @@
"title": "Theme hot-reload in release builds", "title": "Theme hot-reload in release builds",
"status": "open", "status": "open",
"file_path": "decisions/questions-process.md", "file_path": "decisions/questions-process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-19", "id": "Q-19",
@@ -810,7 +810,7 @@
"title": "(withdrawn)", "title": "(withdrawn)",
"status": "resolved", "status": "resolved",
"file_path": "decisions/questions-process.md", "file_path": "decisions/questions-process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-20", "id": "Q-20",
@@ -819,7 +819,7 @@
"title": "Kernel DB service — namespaced SQL access?", "title": "Kernel DB service — namespaced SQL access?",
"status": "open", "status": "open",
"file_path": "decisions/questions-process.md", "file_path": "decisions/questions-process.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-21", "id": "Q-21",
@@ -828,7 +828,7 @@
"title": "Pql absorbs planning vs keeps separate", "title": "Pql absorbs planning vs keeps separate",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-22", "id": "Q-22",
@@ -837,7 +837,7 @@
"title": "Ticket persistence strategy", "title": "Ticket persistence strategy",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-23", "id": "Q-23",
@@ -846,7 +846,7 @@
"title": "SSH-remote development — run clide against a remote workspace", "title": "SSH-remote development — run clide against a remote workspace",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-25", "id": "Q-25",
@@ -855,7 +855,7 @@
"title": "Body text face — mono everywhere vs Josefin Sans UI + mono code", "title": "Body text face — mono everywhere vs Josefin Sans UI + mono code",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-26", "id": "Q-26",
@@ -864,7 +864,7 @@
"title": "Small screen layout (\u003c 1000px)", "title": "Small screen layout (\u003c 1000px)",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-27", "id": "Q-27",
@@ -873,7 +873,7 @@
"title": "Two-editor split", "title": "Two-editor split",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-28", "id": "Q-28",
@@ -882,7 +882,7 @@
"title": "Terminal strip scope — shell only or logs/errors/tests", "title": "Terminal strip scope — shell only or logs/errors/tests",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-29", "id": "Q-29",
@@ -891,7 +891,7 @@
"title": "Branch picker location", "title": "Branch picker location",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-30", "id": "Q-30",
@@ -900,7 +900,7 @@
"title": "Focus behavior when editor is dirty and viewer is peeked", "title": "Focus behavior when editor is dirty and viewer is peeked",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "Q-31", "id": "Q-31",
@@ -909,7 +909,7 @@
"title": "XWayland fallback for frameless — proper Wayland protocol needed", "title": "XWayland fallback for frameless — proper Wayland protocol needed",
"status": "open", "status": "open",
"file_path": "decisions/questions-architecture.md", "file_path": "decisions/questions-architecture.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-2", "id": "R-2",
@@ -919,7 +919,7 @@
"status": "active", "status": "active",
"date": "2026-04-20", "date": "2026-04-20",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-3", "id": "R-3",
@@ -929,7 +929,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-4", "id": "R-4",
@@ -939,7 +939,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-5", "id": "R-5",
@@ -949,7 +949,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-6", "id": "R-6",
@@ -959,7 +959,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-7", "id": "R-7",
@@ -969,7 +969,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-8", "id": "R-8",
@@ -979,7 +979,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-9", "id": "R-9",
@@ -989,7 +989,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-10", "id": "R-10",
@@ -999,7 +999,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-11", "id": "R-11",
@@ -1009,7 +1009,7 @@
"status": "active", "status": "active",
"date": "2026-04-21", "date": "2026-04-21",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
}, },
{ {
"id": "R-12", "id": "R-12",
@@ -1019,7 +1019,7 @@
"status": "active", "status": "active",
"date": "2026-04-22", "date": "2026-04-22",
"file_path": "decisions/rejected.md", "file_path": "decisions/rejected.md",
"synced_at": "2026-05-05 13:07:03" "synced_at": "2026-05-05 13:10:16"
} }
], ],
"decision_refs": [ "decision_refs": [
@@ -2606,19 +2606,19 @@
"id": "T-75", "id": "T-75",
"type": "task", "type": "task",
"title": "PTY: surface errno from forkpty/execve/write/ioctl failures", "title": "PTY: surface errno from forkpty/execve/write/ioctl failures",
"status": "in_progress", "status": "done",
"priority": "high", "priority": "high",
"created_at": "2026-05-05 12:58:59", "created_at": "2026-05-05 12:58:59",
"updated_at": "2026-05-05 13:02:40" "updated_at": "2026-05-05 13:07:29"
}, },
{ {
"id": "T-76", "id": "T-76",
"type": "task", "type": "task",
"title": "PTY: fix resource leaks and reader-isolate races", "title": "PTY: fix resource leaks and reader-isolate races",
"status": "backlog", "status": "in_progress",
"priority": "high", "priority": "high",
"created_at": "2026-05-05 12:58:59", "created_at": "2026-05-05 12:58:59",
"updated_at": "2026-05-05 12:58:59" "updated_at": "2026-05-05 13:07:34"
}, },
{ {
"id": "T-77", "id": "T-77",
@@ -3947,6 +3947,20 @@
"old_value": "backlog", "old_value": "backlog",
"new_value": "in_progress", "new_value": "in_progress",
"changed_at": "2026-05-05 13:02:40" "changed_at": "2026-05-05 13:02:40"
},
{
"ticket_id": "T-75",
"field": "status",
"old_value": "in_progress",
"new_value": "done",
"changed_at": "2026-05-05 13:07:29"
},
{
"ticket_id": "T-76",
"field": "status",
"old_value": "backlog",
"new_value": "in_progress",
"changed_at": "2026-05-05 13:07:34"
} }
] ]
} }
+9
View File
@@ -34,6 +34,15 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
immediate EOF that looked indistinguishable from clean exit. immediate EOF that looked indistinguishable from clean exit.
PTY `write` loops on short writes; both `NativePty.write` and PTY `write` loops on short writes; both `NativePty.write` and
`PtySession.write` now throw `PtyException` on hard errors. `PtySession.write` now throw `PtyException` on hard errors.
- PTY teardown order fixed — kill the child first so the master
fd returns EOF, await the reader isolate exit, then close the
fd. Previously closing the fd while the isolate still polled it
could briefly target a reused fd. Reader isolate spawn errors
in both `NativePty` and `PtySession` are now surfaced via the
output stream instead of silently dropped. `_recvFdAsync` no
longer leaks the `ReceivePort` when `Isolate.spawn` throws, and
`PtySession.spawn` closes the master fd if any post-receive
step fails.
### Security ### Security
+56 -3
View File
@@ -90,6 +90,13 @@ class NativePty {
final _out = StreamController<Uint8List>.broadcast(); final _out = StreamController<Uint8List>.broadcast();
bool _dead = false; bool _dead = false;
/// Tracks the reader isolate's spawn — close() awaits this before
/// tearing down so we never race a still-spawning isolate.
Future<void>? _readerReady;
Isolate? _readerIsolate;
ReceivePort? _readerPort;
Completer<void>? _readerExited;
NativePty._(this._fd, this.pid); NativePty._(this._fd, this.pid);
/// Byte stream of data produced by the child. /// Byte stream of data produced by the child.
@@ -222,13 +229,32 @@ class NativePty {
// -- I/O ------------------------------------------------------------------ // -- I/O ------------------------------------------------------------------
void _spawnReader() async { void _spawnReader() {
_readerReady = _spawnReaderAsync();
}
Future<void> _spawnReaderAsync() async {
final rp = ReceivePort(); final rp = ReceivePort();
await Isolate.spawn(_readLoop, (rp.sendPort, _fd)); _readerPort = rp;
_readerExited = Completer<void>();
try {
_readerIsolate = await Isolate.spawn(_readLoop, (rp.sendPort, _fd));
} catch (e) {
// Surface the spawn failure instead of leaving the PTY in a
// half-alive state where output never flows but isClosed=false.
_dead = true;
if (!_out.isClosed) _out.addError(PtyException('reader-spawn', '$e'));
rp.close();
_readerPort = null;
if (!_readerExited!.isCompleted) _readerExited!.complete();
return;
}
rp.listen((msg) { rp.listen((msg) {
if (msg == null) { if (msg == null) {
if (!_out.isClosed) _out.close(); if (!_out.isClosed) _out.close();
rp.close(); rp.close();
_readerPort = null;
if (!_readerExited!.isCompleted) _readerExited!.complete();
_reap(); _reap();
} else { } else {
if (!_out.isClosed) _out.add(msg as Uint8List); if (!_out.isClosed) _out.add(msg as Uint8List);
@@ -330,12 +356,39 @@ class NativePty {
} }
/// Kill the child and release resources. /// Kill the child and release resources.
///
/// Order matters: kill the child first so its slave PTY closes,
/// causing the master fd to return EOF. The reader isolate sees
/// EOF and exits cleanly. Only then do we close the master fd —
/// closing it before the isolate exits creates a window where the
/// fd number could be reused and the isolate would briefly poll
/// the wrong file.
Future<void> close() async { Future<void> close() async {
if (_dead) return; if (_dead) return;
_dead = true; _dead = true;
_nativeClose(_fd);
// Make sure the reader is fully spawned before we tear it down —
// otherwise close() racing with start() leaves an orphan isolate.
await _readerReady;
_nativeKill(pid, _kSighup); _nativeKill(pid, _kSighup);
_nativeKill(pid, 9); _nativeKill(pid, 9);
// Wait for the isolate to send `null` (EOF) — confirms it has
// exited its poll loop and won't touch the fd again.
if (_readerExited != null) {
await _readerExited!.future.timeout(
const Duration(milliseconds: 500),
onTimeout: () {},
);
}
_nativeClose(_fd);
_readerIsolate?.kill(priority: Isolate.immediate);
_readerIsolate = null;
_readerPort?.close();
_readerPort = null;
final s = calloc<ffi.Int32>(); final s = calloc<ffi.Int32>();
_waitpid(pid, s, 0); _waitpid(pid, s, 0);
calloc.free(s); calloc.free(s);
+55 -26
View File
@@ -132,31 +132,44 @@ class PtySession {
// Receive the master fd over the parent side of the socketpair. // Receive the master fd over the parent side of the socketpair.
// recvFd blocks until ptyc sends — run in a child isolate so the // recvFd blocks until ptyc sends — run in a child isolate so the
// calling isolate's event loop stays responsive. // calling isolate's event loop stays responsive.
final masterFd = await _recvFdAsync(parentSock); final int masterFd;
try {
// Apply initial winsize (ptyc already did this, but doing it masterFd = await _recvFdAsync(parentSock);
// again from Dart confirms the wire + gives a place to call it } catch (_) {
// when resize() lands). proc.kill();
libc.setWinsize(masterFd, cols, rows); rethrow;
// Drain ptyc's stdout to parse the success envelope. We don't
// strictly need it — the fd arriving is proof-of-life — but
// draining avoids a PIPE accumulating.
final stdoutLine = await proc.stdout.transform(const Utf8Decoder()).transform(const LineSplitter()).first.timeout(const Duration(seconds: 5));
final pid = _extractPid(stdoutLine);
final code = await proc.exitCode;
if (code != 0) {
final stderr = await proc.stderr.transform(const Utf8Decoder()).join();
libc.close(masterFd);
throw PtyException('ptyc', 'ptyc exited with code $code: $stderr');
} }
return PtySession._(pid: pid, masterFd: masterFd); // Once we own masterFd, every error path below must close it
// before rethrowing. Wrap the rest of the spawn in its own
// try/catch so the cleanup is centralized.
try {
libc.setWinsize(masterFd, cols, rows);
final stdoutLine = await proc.stdout
.transform(const Utf8Decoder())
.transform(const LineSplitter())
.first
.timeout(const Duration(seconds: 5));
final pid = _extractPid(stdoutLine);
final code = await proc.exitCode;
if (code != 0) {
final stderr = await proc.stderr.transform(const Utf8Decoder()).join();
libc.close(masterFd);
throw PtyException('ptyc', 'ptyc exited with code $code: $stderr');
}
return PtySession._(pid: pid, masterFd: masterFd);
} catch (_) {
libc.close(masterFd);
rethrow;
}
} finally { } finally {
// parent keeps its own fd until the session is closed; ptyc-side // parent keeps its own fd until the session is closed; ptyc-side
// fd is released either way (ptyc has exited by now). // fd is released either way (ptyc has exited by now).
if (childSock >= 0) libc.close(childSock); if (childSock >= 0) libc.close(childSock);
if (parentSock >= 0) libc.close(parentSock);
pkg_ffi.calloc.free(sv); pkg_ffi.calloc.free(sv);
} }
} }
@@ -254,12 +267,16 @@ class PtySession {
/// stall the calling isolate's event loop. /// stall the calling isolate's event loop.
static Future<int> _recvFdAsync(int socketFd) async { static Future<int> _recvFdAsync(int socketFd) async {
final port = ReceivePort(); final port = ReceivePort();
final iso = await Isolate.spawn(_recvFdEntry, _RecvFdArgs(socketFd, port.sendPort)); Isolate? iso;
final result = await port.first; try {
iso.kill(priority: Isolate.immediate); iso = await Isolate.spawn(_recvFdEntry, _RecvFdArgs(socketFd, port.sendPort));
port.close(); final result = await port.first;
if (result is int) return result; if (result is int) return result;
throw PtyException('recvFd', '$result'); throw PtyException('recvFd', '$result');
} finally {
iso?.kill(priority: Isolate.immediate);
port.close();
}
} }
static void _recvFdEntry(_RecvFdArgs args) { static void _recvFdEntry(_RecvFdArgs args) {
@@ -288,7 +305,19 @@ class PtySession {
Isolate.spawn<_ReaderArgs>( Isolate.spawn<_ReaderArgs>(
_readerEntrypoint, _readerEntrypoint,
_ReaderArgs(fd: _masterFd, sendPort: port.sendPort), _ReaderArgs(fd: _masterFd, sendPort: port.sendPort),
).then((iso) => _readerIsolate = iso); ).then(
(iso) => _readerIsolate = iso,
onError: (Object e) {
// Spawn failure leaves the session unable to ever produce
// output. Surface the error and mark the controller closed
// so consumers don't hang waiting on the stream.
if (!_outputCtrl.isClosed) {
_outputCtrl.addError(PtyException('reader-spawn', '$e'));
_outputCtrl.close();
}
if (!_readerExited.isCompleted) _readerExited.complete();
},
);
} }
// -- request builder ------------------------------------------------------ // -- request builder ------------------------------------------------------