Files
sdk/sdk/lib/_internal/vm/bin/stdio_patch.dart
T
Jonas Termansen 83850ac5fa [io] Don't restore terminal state on exit.
This is breaking change #45630.

The Dart VM has until now restored the terminal settings upon exit to
their initial values for stdin, stdout, and stderr. This change removes
that automatic behavior in favor of having the program do the
restoration. Previously the intention was that dart programs can
enable/disable echoing and line buffering and not worry about restoring
the original settings.

However, the VM doing so unconditionally leads to undesirable behavior
e.g. when the program does not care about terminal settings and is
sharing a process group with a program that does care. E.g. if dart's
output is piped into less(1), then there is a race condition where dart
might see the raw terminal settings set by less(1), and if the dart VM
exits after less(1) has exited, then it will restore the raw terminal
settings, leaving the user with a seemingly defective shell with echo
disabled. This race condition can be reproduced using:

    cat > yes.dart << EOF
    main() {
      for (int i = 0; i < 1000000; i++) {
        print("yes");
      }
    }
    EOF
    stty; (sleep 1 && dart yes.dart) | less; stty; stty sane

The user will end up with a shell with echo behavior disabled. The stty
command shows the current terminal settings, where the difference can be
seen, and 'stty sane' fixes the settings before returning to the shell
prompt. The 'stty sane' call can be omitted to see the defective shell
prompt.

This change removes the terminal restoring behavior (added in Dart
2.0.0) and instead asks applications to do the restoration themselves.
The new design matches how programs in other programming languages
implement interactive input that changes terminal settings.

Furthermore the `echoMode` setting now only controls the `echo` local
mode and no longer sets the `echonl` local mode on POSIX systems (which
controls whether newline are echoed even if the regular echo mode is
disabled). The `echonl` local mode is usually turned off in common shell
environments. Programs that wish to control the `echonl` local mode can
use the new `echoNewlineMode` setting. This change is required to
prevent the reoccurence of #30318 when programs manually restore
`echoMode`.

Windows has further considerations: It also saves the console code pages
and restore them if they were not UTF-8. This behavior is retained as it
is useful and needed for Dart's output to function properly. ANSI output
sequences are also turned on via ENABLE_VIRTUAL_TERMINAL_PROCESSING,
which is slightly changed in this change to only rsetore that setting if
it wasn't already on for consistency.

Closes https://github.com/dart-lang/sdk/issues/36453
Closes https://github.com/dart-lang/sdk/issues/45630

TEST=Reproduced with less as above

Change-Id: I2991f9c7f47b97fe475c1ad6edeb769024f8d0db
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/190484
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
Reviewed-by: Jonas Termansen <sortie@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
2022-05-31 15:36:33 +00:00

197 lines
5.6 KiB
Dart

// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// part of "common_patch.dart";
@patch
class _StdIOUtils {
@patch
static Stdin _getStdioInputStream(int fd) {
final type = _getStdioHandleType(fd);
if (type is OSError) {
throw FileSystemException(
"Failed to get type of stdio handle (fd $fd)", "", type);
}
switch (type) {
case _stdioHandleTypeTerminal:
case _stdioHandleTypePipe:
case _stdioHandleTypeSocket:
case _stdioHandleTypeOther:
return new Stdin._(new _Socket._readPipe(fd), fd);
case _stdioHandleTypeFile:
return new Stdin._(new _FileStream.forStdin(), fd);
}
throw new UnsupportedError("Unexpected handle type $type");
}
@patch
static _getStdioOutputStream(int fd) {
final type = _getStdioHandleType(fd);
if (type is OSError) {
throw FileSystemException(
"Failed to get type of stdio handle (fd $fd)", "", type);
}
return new Stdout._(new IOSink(new _StdConsumer(fd)), fd);
}
@patch
static int? _socketType(Socket socket) {
if (socket is _Socket) return _nativeSocketType(socket._nativeSocket);
return null;
}
static int _nativeSocketType(_NativeSocket nativeSocket) {
var result = _getSocketType(nativeSocket);
if (result is OSError) {
throw new FileSystemException("Error retrieving socket type", "", result);
}
return result;
}
@patch
@pragma("vm:external-name", "File_GetStdioHandleType")
external static _getStdioHandleType(int fd);
}
@patch
class Stdin {
@patch
int readByteSync() {
var result = _readByte(_fd);
if (result is OSError) {
throw new StdinException("Error reading byte from stdin", result);
}
return result;
}
@patch
bool get echoMode {
var result = _echoMode(_fd);
if (result is OSError) {
throw new StdinException("Error getting terminal echo mode", result);
}
return result;
}
@patch
void set echoMode(bool enabled) {
if (!_EmbedderConfig._maySetEchoMode) {
throw new UnsupportedError(
"This embedder disallows setting Stdin.echoMode");
}
var result = _setEchoMode(_fd, enabled);
if (result is OSError) {
throw new StdinException("Error setting terminal echo mode", result);
}
}
@patch
bool get echoNewlineMode {
var result = _echoNewlineMode(_fd);
if (result is OSError) {
throw new StdinException(
"Error getting terminal echo newline mode", result);
}
return result;
}
@patch
void set echoNewlineMode(bool enabled) {
if (!_EmbedderConfig._maySetEchoNewlineMode) {
throw new UnsupportedError(
"This embedder disallows setting Stdin.echoNewlineMode");
}
var result = _setEchoNewlineMode(_fd, enabled);
if (result is OSError) {
throw new StdinException(
"Error setting terminal echo newline mode", result);
}
}
@patch
bool get lineMode {
var result = _lineMode(_fd);
if (result is OSError) {
throw new StdinException("Error getting terminal line mode", result);
}
return result;
}
@patch
void set lineMode(bool enabled) {
if (!_EmbedderConfig._maySetLineMode) {
throw new UnsupportedError(
"This embedder disallows setting Stdin.lineMode");
}
var result = _setLineMode(_fd, enabled);
if (result is OSError) {
throw new StdinException("Error setting terminal line mode", result);
}
}
@patch
bool get supportsAnsiEscapes {
var result = _supportsAnsiEscapes(_fd);
if (result is OSError) {
throw new StdinException("Error determining ANSI support", result);
}
return result;
}
@pragma("vm:external-name", "Stdin_GetEchoMode")
external static _echoMode(int fd);
@pragma("vm:external-name", "Stdin_SetEchoMode")
external static _setEchoMode(int fd, bool enabled);
@pragma("vm:external-name", "Stdin_GetEchoNewlineMode")
external static _echoNewlineMode(int fd);
@pragma("vm:external-name", "Stdin_SetEchoNewlineMode")
external static _setEchoNewlineMode(int fd, bool enabled);
@pragma("vm:external-name", "Stdin_GetLineMode")
external static _lineMode(int fd);
@pragma("vm:external-name", "Stdin_SetLineMode")
external static _setLineMode(int fd, bool enabled);
@pragma("vm:external-name", "Stdin_ReadByte")
external static _readByte(int fd);
@pragma("vm:external-name", "Stdin_AnsiSupported")
external static _supportsAnsiEscapes(int fd);
}
@patch
class Stdout {
@patch
bool _hasTerminal(int fd) => _getTerminalSize(fd) is List;
@patch
int _terminalColumns(int fd) => _terminalSize(fd)[0];
@patch
int _terminalLines(int fd) => _terminalSize(fd)[1];
static List _terminalSize(int fd) {
var size = _getTerminalSize(fd);
if (size is! List) {
throw new StdoutException("Could not get terminal size", size);
}
return size;
}
@pragma("vm:external-name", "Stdout_GetTerminalSize")
external static _getTerminalSize(int fd);
@patch
static bool _supportsAnsiEscapes(int fd) {
var result = _getAnsiSupported(fd);
if (result is! bool) {
throw new StdoutException("Error determining ANSI support", result);
}
return result;
}
@pragma("vm:external-name", "Stdout_AnsiSupported")
external static _getAnsiSupported(int fd);
}
@pragma("vm:external-name", "Socket_GetStdioHandle")
external bool _getStdioHandle(_NativeSocket socket, int num);
@pragma("vm:external-name", "Socket_GetType")
external _getSocketType(_NativeSocket nativeSocket);