Files
clide/tools/windows-verify/conpty_orphan_probe.dart
T
jpmschweitzerandClaude Opus 4.8 2953207960 ci(soak): abrupt-death ConPTY orphan probe (the failure-mode the clean soak misses)
The clean-path soak found no leak on windows-latest — because orderly close()
reaps every host. That never exercises the freeze hypothesis (T-424), which is
the parent dying WITHOUT teardown while a child is live.

This probe does. conpty_orphan_probe.dart starts N real WindowsPty sessions on
long-lived children and blocks WITHOUT ever calling close(); soak-conpty-kill.ps1
force-kills only the dart.exe parent (taskkill /F, no /T) once the hosts are up
and counts the conhost/OpenConsole/cmd processes that SURVIVE. Absent a
kill-on-close Job Object, abrupt parent death should orphan them — a survivor
count that climbs across cycles is the leak signature. The same probe will prove
the T-424 fix: with the job, survivors should drop to ~0.

Wired as a second job in windows-soak.yml (workflow_dispatch + when the kit
changes). Diagnostic only, never a gate, always exits 0; publishes the verdict
to the job summary and uploads the CSV. Not part of the regular test suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:58:14 +02:00

64 lines
2.8 KiB
Dart

/// ConPTY orphan probe — the ABRUPT-DEATH half of the windows-verify kit.
///
/// `soak-conpty.ps1` measures the CLEAN path: dart exits normally, the ConPTY
/// teardown (`close()` → ClosePseudoConsole → handle release) runs, and the
/// hosts are reaped. On GitHub's windows-latest that path showed NO leak —
/// orderly shutdown reclaims everything. But the freeze hypothesis (T-424) is
/// not about orderly shutdown; it is about the parent dying WITHOUT teardown
/// (a crash, a Ctrl-C, a wedged reader isolate) while the child is still live.
///
/// This probe exercises exactly that. It starts [count] real [WindowsPty]
/// sessions — the production backend, no mocks — each running a long-lived
/// child, prints a READY line carrying the dart pid + each child pid, then
/// blocks forever and NEVER calls `close()`. Its companion driver
/// (`soak-conpty-kill.ps1`) force-kills this dart.exe (`taskkill /F`, NOT
/// `/T`, so only the parent dies) once the children are up, then counts the
/// conhost / OpenConsole / cmd processes that SURVIVE the parent's death.
///
/// Because the children are not placed in a kill-on-close Job Object, abrupt
/// parent death is expected to orphan them — that is the leak this probe is
/// built to expose. The same probe will later PROVE the T-424 fix: once each
/// child lives in a JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE job, killing the
/// parent should take the job — and every host — down with it, and the
/// survivor count should drop to zero.
library;
import 'dart:io';
import 'package:clide/src/pty/windows_pty.dart';
Future<void> main(List<String> args) async {
if (!Platform.isWindows) {
stderr.writeln('conpty_orphan_probe: Windows only (ConPTY).');
exit(2);
}
final count = args.isNotEmpty ? (int.tryParse(args.first) ?? 1) : 1;
final sessions = <WindowsPty>[];
for (var i = 0; i < count; i++) {
final s = WindowsPty.start(
executable: 'cmd.exe',
// A long-lived child so the ConPTY host stays alive across the whole
// kill window. `ping -n 600` loops for ~10 min with no extra deps.
arguments: ['/c', 'ping -n 600 127.0.0.1'],
columns: 80,
rows: 24,
environment: {...Platform.environment, 'TERM': 'xterm-256color'},
);
// Drain output so the reader isolate is actively pumping, closest to a
// real live pane. Discard the bytes.
s.output.listen((_) {}, onError: (_) {});
sessions.add(s);
}
// Signal the driver that the children are up. It waits for the host count
// to rise, then kills us.
stdout.writeln('PROBE READY dart_pid=$pid child_pids=${sessions.map((s) => s.pid).join(',')}');
await stdout.flush();
// Block WITHOUT ever calling close() — the driver kills us mid-sleep. That
// missing teardown is the entire point; do not add a finally/close here.
await Future<void>.delayed(const Duration(minutes: 10));
exit(0);
}