feat(pty): FFI breadcrumbs around the syscalls that wedge (T-434)
The freeze hypothesis is a wedged FFI call — a reader isolate blocked forever
in ReadFile, a waiter in WaitForSingleObject, Isolate.kill unable to interrupt
either. To NAME the wedge after a power-cycle, each backend now drops a
breadcrumb before/after every risky syscall.
- pty_log.dart (new, Flutter-free, tested): PtyLog — an injectable, no-op-by-
default breadcrumb hook for the MAIN isolate (wired to the kernel Logger,
source 'conpty'/'pty' = an eager FileLogSink source) — and IsolateCrumbFile,
which the SPAWNED reader/waiter isolates use to open their OWN append handle
and flushSync per line, so a wedged isolate's last crumb survives even a
frozen main isolate (the whole point). Bounded by a truncating size cap.
- native_pty.dart + windows_pty.dart: crumbs around posix_spawn/read and
CreatePseudoConsole/CreateProcessW/ReadFile/WaitForSingleObject; the reader/
waiter isolates carry a sendable crumb path + verbose flag. Per-syscall crumbs
only at debug/trace; lifecycle crumbs always.
- Wiring: startPtySession → PaneRegistry → buildDispatcher build the PtyLog from
the kernel Logger + a crumb file under logDirectory(); verbose follows the log
level. Default everywhere is PtyLog.none — zero behaviour change off the wire.
Tested: PtyLog/IsolateCrumbFile units (cap-truncation, append, no-op) + an
end-to-end real-PTY test asserting the reader isolate writes its own crumbs
('reader started' / 'read -> n=' / 'reader exiting'), which validates the
identical Windows structure that can't run here. Coverage gate 95.10%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:clide/src/pty/errors.dart';
|
||||
import 'package:clide/src/pty/native_pty.dart';
|
||||
import 'package:clide/src/pty/pty_log.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../helpers/timeouts.dart';
|
||||
@@ -41,6 +42,40 @@ void main() {
|
||||
expect(got, contains('hello-pty'));
|
||||
});
|
||||
|
||||
test('emits FFI breadcrumbs to the main callback + the reader isolate crumb file (T-434)', tags: ['pty'], () async {
|
||||
final dir = Directory.systemTemp.createTempSync('clide-pty-crumb-');
|
||||
addTearDown(() {
|
||||
if (dir.existsSync()) dir.deleteSync(recursive: true);
|
||||
});
|
||||
final crumbPath = '${dir.path}/pty.crumbs';
|
||||
final mainCrumbs = <String>[];
|
||||
|
||||
final s = NativePty.start(
|
||||
executable: '/bin/sh',
|
||||
arguments: ['-c', 'echo crumb-test'],
|
||||
columns: 80,
|
||||
rows: 24,
|
||||
workingDirectory: '/',
|
||||
environment: {...Platform.environment, 'TERM': 'xterm-256color'},
|
||||
log: PtyLog(onCrumb: mainCrumbs.add, crumbPath: crumbPath, verbose: true),
|
||||
);
|
||||
addTearDown(s.close);
|
||||
|
||||
// Drain to EOF so the reader isolate runs its full lifecycle (it writes
|
||||
// its 'exiting' crumb + closes the file before sending the EOF we await).
|
||||
await s.output.drain<void>().timeout(ioTimeout, onTimeout: () {});
|
||||
|
||||
// Main-isolate crumbs captured the spawn syscall sequence.
|
||||
expect(mainCrumbs.join('\n'), contains('posix_spawn'));
|
||||
|
||||
// The SPAWNED reader isolate wrote its own crumbs to its own handle.
|
||||
final crumbs = File(crumbPath).readAsStringSync();
|
||||
expect(crumbs, contains('[pty.reader] reader started'));
|
||||
expect(crumbs, contains('[pty.reader] reader exiting'));
|
||||
// verbose:true → per-read crumbs around the (potentially blocking) read.
|
||||
expect(crumbs, contains('[pty.reader] read -> n='));
|
||||
});
|
||||
|
||||
test('write sends keystrokes to child', tags: ['pty'], () async {
|
||||
final s = NativePty.start(
|
||||
executable: '/bin/sh',
|
||||
|
||||
Reference in New Issue
Block a user