Closes the observability loop: the log + breadcrumb + watchdog files are now collected by CI so a wedged run leaves downloadable evidence instead of nothing. - logDirectory(): CLIDE_LOG_DIR overrides the per-platform default, so CI can point the logs at an uploadable workspace dir (and tests at a temp dir). Now takes an injectable env map; tested. - test_app.dart: when CLIDE_LOG_DIR is set, the testmode harness tees its logger to a FileLogSink + spawns the watchdog (off by default — normal run-testmode keeps the stderr-only path, no isolate). _say breadcrumbs each test into the file. - conpty_orphan_probe.dart: with CLIDE_LOG_DIR set it passes a verbose PtyLog, so when soak-conpty-kill.ps1 force-kills the parent, the reader/waiter isolates' LAST crumb is fsynced to disk — naming what the wedged isolate was doing at the instant of death. - bundle-smoke job: runs the real release app with CLIDE_LOG=debug + CLIDE_LOG_DIR, uploads clide-logs (watchdog heartbeat/sample + FileLogSink) in an always() step. - windows-soak kill-probe job: sets CLIDE_LOG_DIR, uploads the FFI crumbs. Coverage gate 95.08%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.4 KiB
Dart
73 lines
3.4 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/pty_log.dart';
|
|
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;
|
|
|
|
// When CLIDE_LOG_DIR is set (the soak workflow sets it), emit FFI breadcrumbs
|
|
// so that when soak-conpty-kill.ps1 force-kills this process mid-life, the
|
|
// reader/waiter isolates' LAST crumb (e.g. "ReadFile enter") is on disk —
|
|
// CI then uploads it, naming what the reader was doing when killed (T-436).
|
|
final logDir = Platform.environment['CLIDE_LOG_DIR'];
|
|
final ptyLog = (logDir == null || logDir.isEmpty) ? PtyLog.none : PtyLog(crumbPath: '$logDir/clide-pty.crumbs.log', verbose: true);
|
|
|
|
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'},
|
|
log: ptyLog,
|
|
);
|
|
// 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);
|
|
}
|