async recvFd + backend terminal test (sandbox blocks ptyc exec)
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped

PtySession.spawn() now runs recvFd in a child isolate via
Isolate.spawn so the blocking FFI call doesn't stall the backend
isolate's event loop.

Terminal testmode test spawns a real backend isolate, opens a project,
and sends pane.spawn via IPC — the exact same path the full app uses.
Currently fails: ptyc successfully starts but its fork+execvp is
blocked by the macOS sandbox ("Operation not permitted"). The SBPL
allows /bin/zsh exec from the app process, but ptyc's child process
may not inherit the exec permission, or the PTYC_SOCK_FD is not
inherited by the ptyc child (Dart Process.start fd inheritance on
macOS).

IsolateClient.events getter exposed for test access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-04-26 15:55:29 +02:00
co-authored by Claude Opus 4.6
parent 8f6ab1ac95
commit 45e8132a41
4 changed files with 88 additions and 10 deletions
+30 -1
View File
@@ -29,6 +29,12 @@ import 'errors.dart';
import 'ffi/libc.dart' as libc;
import 'ffi/scm_rights.dart' as scm;
class _RecvFdArgs {
const _RecvFdArgs(this.socketFd, this.sendPort);
final int socketFd;
final SendPort sendPort;
}
/// A running PTY child plus its master-fd plumbing.
class PtySession {
PtySession._({
@@ -124,7 +130,9 @@ class PtySession {
await proc.stdin.close();
// Receive the master fd over the parent side of the socketpair.
final masterFd = scm.recvFd(parentSock);
// recvFd blocks until ptyc sends — run in a child isolate so the
// calling isolate's event loop stays responsive.
final masterFd = await _recvFdAsync(parentSock);
// Apply initial winsize (ptyc already did this, but doing it
// again from Dart confirms the wire + gives a place to call it
@@ -229,6 +237,27 @@ class PtySession {
if (!_outputCtrl.isClosed) await _outputCtrl.close();
}
/// Run recvFd in a child isolate so the blocking FFI call doesn't
/// stall the calling isolate's event loop.
static Future<int> _recvFdAsync(int socketFd) async {
final port = ReceivePort();
final iso = await Isolate.spawn(_recvFdEntry, _RecvFdArgs(socketFd, port.sendPort));
final result = await port.first;
iso.kill(priority: Isolate.immediate);
port.close();
if (result is int) return result;
throw PtyException('recvFd', '$result');
}
static void _recvFdEntry(_RecvFdArgs args) {
try {
final fd = scm.recvFd(args.socketFd);
args.sendPort.send(fd);
} catch (e) {
args.sendPort.send('error: $e');
}
}
// ---------------------------------------------------------------- //
void _startReader() {