-
Notifications
You must be signed in to change notification settings - Fork 0
fix(core): restore terminal state after inherited-stdio profiling #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| import 'dart:io'; | ||
|
|
||
| /// The stdin terminal controls that can be captured without platform code. | ||
| abstract interface class TerminalInput { | ||
| bool get hasTerminal; | ||
| bool get echoMode; | ||
| set echoMode(bool value); | ||
| bool get lineMode; | ||
| set lineMode(bool value); | ||
| bool get echoNewlineMode; | ||
| set echoNewlineMode(bool value); | ||
|
Comment on lines
+5
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Document the public members of The repository guideline applies to all Dart files and requires 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /// A terminal-backed output sink used for terminal cleanup. | ||
| abstract interface class TerminalOutput { | ||
| bool get hasTerminal; | ||
| void write(String value); | ||
| Future<void> flush(); | ||
| } | ||
|
|
||
| final class _StdinTerminalInput implements TerminalInput { | ||
| const _StdinTerminalInput(); | ||
|
|
||
| @override | ||
| bool get hasTerminal => stdin.hasTerminal; | ||
| @override | ||
| bool get echoMode => stdin.echoMode; | ||
| @override | ||
| set echoMode(bool value) => stdin.echoMode = value; | ||
| @override | ||
| bool get lineMode => stdin.lineMode; | ||
| @override | ||
| set lineMode(bool value) => stdin.lineMode = value; | ||
| @override | ||
| bool get echoNewlineMode => stdin.echoNewlineMode; | ||
| @override | ||
| set echoNewlineMode(bool value) => stdin.echoNewlineMode = value; | ||
| } | ||
|
|
||
| final class _IoTerminalOutput implements TerminalOutput { | ||
| const _IoTerminalOutput(this._sink, this._hasTerminal); | ||
|
|
||
| final IOSink _sink; | ||
| final bool Function() _hasTerminal; | ||
|
|
||
| @override | ||
| bool get hasTerminal => _hasTerminal(); | ||
| @override | ||
| void write(String value) => _sink.write(value); | ||
| @override | ||
| Future<void> flush() => _sink.flush(); | ||
| } | ||
|
|
||
| /// Captures and restores terminal state for an inherited-stdio session. | ||
| final class TerminalLifecycle { | ||
| TerminalLifecycle._(this._modes, this._outputs); | ||
|
|
||
| final List<_CapturedMode> _modes; | ||
| final List<TerminalOutput> _outputs; | ||
|
|
||
| /// Captures supported stdin modes and terminal-backed output streams. | ||
| /// | ||
| /// Unsupported mode accessors are skipped independently. ANSI cleanup is | ||
| /// never sent to redirected streams. Windows hosts without virtual-terminal | ||
| /// processing enabled cannot interpret ANSI sequences; enabling that native | ||
| /// console feature is outside the Dart standard API. | ||
| static Future<TerminalLifecycle> capture({ | ||
| TerminalInput? input, | ||
| List<TerminalOutput>? outputs, | ||
| }) async { | ||
| final actualInput = input ?? const _StdinTerminalInput(); | ||
| final actualOutputs = | ||
| outputs ?? | ||
| <TerminalOutput>[ | ||
| _IoTerminalOutput(stdout, () => stdout.hasTerminal), | ||
| _IoTerminalOutput(stderr, () => stderr.hasTerminal), | ||
| ]; | ||
| final modes = <_CapturedMode>[]; | ||
| try { | ||
| if (actualInput.hasTerminal) { | ||
| for (final mode in _terminalModes(actualInput)) { | ||
| try { | ||
| modes.add(_CapturedMode(mode, mode.read())); | ||
| } catch (_) { | ||
| // Some terminals expose only a subset of the controls. | ||
| } | ||
| } | ||
| } | ||
| } catch (_) { | ||
| // A stream which cannot report terminal status is not controllable. | ||
| } | ||
| return TerminalLifecycle._(modes, [ | ||
| for (final output in actualOutputs) | ||
| if (_hasTerminal(output)) output, | ||
| ]); | ||
| } | ||
|
|
||
| /// Restores captured modes and common interactive terminal UI state. | ||
| /// | ||
| /// Each mode and output is independent. Shared stdout/stderr sinks are | ||
| /// flushed but never closed. | ||
| Future<void> restore() async { | ||
| for (final mode in _modes) { | ||
| try { | ||
| mode.write(mode.value); | ||
| } catch (_) { | ||
| // One unsupported or failing setter must not block the others. | ||
| } | ||
| } | ||
|
|
||
| const cleanupSequences = [ | ||
| '\x1b[?1000l\x1b[?1002l\x1b[?1003l', | ||
| '\x1b[?1006l\x1b[?1015l\x1b[?1016l', | ||
| '\x1b[?1004l\x1b[?2004l\x1b[?2026l', | ||
| '\x1b[?25h\x1b[0m\x1b[r', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L57-L58 Useful? React with 👍 / 👎. |
||
| // Leave the primary screen and its scrollback intact. | ||
| '\x1b[?1049l\x1b[?1047l\x1b[?47l', | ||
| ]; | ||
| for (final output in _outputs) { | ||
| for (final sequence in cleanupSequences) { | ||
| try { | ||
| output.write(sequence); | ||
| await output.flush(); | ||
| } catch (_) { | ||
| // A broken output must not prevent another output being cleaned. | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| bool _hasTerminal(TerminalOutput output) { | ||
| try { | ||
| return output.hasTerminal; | ||
| } catch (_) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| Iterable<_TerminalMode> _terminalModes(TerminalInput input) sync* { | ||
| yield _TerminalMode(() => input.echoMode, (value) { | ||
| input.echoMode = value; | ||
| }); | ||
| yield _TerminalMode(() => input.lineMode, (value) { | ||
| input.lineMode = value; | ||
| }); | ||
| yield _TerminalMode(() => input.echoNewlineMode, (value) { | ||
| input.echoNewlineMode = value; | ||
| }); | ||
| } | ||
|
|
||
| final class _TerminalMode { | ||
| const _TerminalMode(this.read, this.write); | ||
|
|
||
| final bool Function() read; | ||
| final void Function(bool value) write; | ||
| } | ||
|
|
||
| final class _CapturedMode { | ||
| const _CapturedMode(this.mode, this.value); | ||
|
|
||
| final _TerminalMode mode; | ||
| final bool value; | ||
|
|
||
| void write(bool value) => mode.write(value); | ||
| bool read() => mode.read(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add a
ProfileRunnertimeout test for SIGKILL escalation.No existing
ProfileRunnertest setsProfileRunRequest.runDurationor uses a target that survives SIGTERM. The PTY probe kills its child directly withtarget.kill(ProcessSignal.sigterm), so it does not exercise_terminateProcessWithEscalation. A regression in the SIGKILL fallback could therefore pass the current tests. Add a runner test that usesrunDuration, ignores SIGTERM, and asserts bounded completion after SIGKILL escalation.🤖 Prompt for AI Agents