diff --git a/CHANGELOG.md b/CHANGELOG.md index 6195c9ec283..2eab9df3a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,52 @@ them, you must set the lower bound on the SDK constraint for your package to - Add `connectionState` attribute and `connectionstatechange` listener to `RtcPeerConnection`. +#### `dart:io` + +- **Breaking Change** [#45630][]: The Dart VM no longer automatically restores + the initial terminal settings upon exit. Programs that change the `Stdin` + settings `lineMode` and `echoMode` are now responsible for restoring the + settings upon program exit. E.g. a program disabling `echoMode` will now + need to restore the setting itself and handle exiting by the appropriate + signals if desired: + + ```dart + import 'dart:io'; + import 'dart:async'; + + main() { + bool echoWasEnabled = stdin.echoMode; + try { + late StreamSubscription subscription; + subscription = ProcessSignal.sigint.watch().listen((ProcessSignal signal) { + stdin.echoMode = echoWasEnabled; + subscription.cancel(); + Process.killPid(pid, signal); /* Die by the signal. */ + }); + stdin.echoMode = false; + } finally { + stdin.echoMode = echoWasEnabled; + } + } + ``` + + This change is needed to fix [#36453][] where the dart programs not caring + about the terminal settings can inadverently corrupt the terminal settings + when e.g. piping into less. + + 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. + + The Windows console code pages (if not UTF-8) and ANSI escape code support + (if disabled) remain restored when the VM exits. + +[#45630]: https://github.com/dart-lang/sdk/issues/45630 +[#36453]: https://github.com/dart-lang/sdk/issues/36453 + #### `dart:js_util` - Added `dartify` and a number of minor helper functions. diff --git a/runtime/bin/console_posix.cc b/runtime/bin/console_posix.cc index 17116da2337..f318ae1ef6a 100644 --- a/runtime/bin/console_posix.cc +++ b/runtime/bin/console_posix.cc @@ -18,71 +18,10 @@ namespace dart { namespace bin { -class PosixConsole { - public: - static const tcflag_t kInvalidFlag = -1; - - static void Initialize() { - SaveMode(STDOUT_FILENO, &stdout_initial_c_lflag_); - SaveMode(STDERR_FILENO, &stderr_initial_c_lflag_); - SaveMode(STDIN_FILENO, &stdin_initial_c_lflag_); - } - - static void Cleanup() { - RestoreMode(STDOUT_FILENO, stdout_initial_c_lflag_); - RestoreMode(STDERR_FILENO, stderr_initial_c_lflag_); - RestoreMode(STDIN_FILENO, stdin_initial_c_lflag_); - ClearLFlags(); - } - - private: - static tcflag_t stdout_initial_c_lflag_; - static tcflag_t stderr_initial_c_lflag_; - static tcflag_t stdin_initial_c_lflag_; - - static void ClearLFlags() { - stdout_initial_c_lflag_ = kInvalidFlag; - stderr_initial_c_lflag_ = kInvalidFlag; - stdin_initial_c_lflag_ = kInvalidFlag; - } - - static void SaveMode(intptr_t fd, tcflag_t* flag) { - ASSERT(flag != NULL); - struct termios term; - int status = TEMP_FAILURE_RETRY(tcgetattr(fd, &term)); - if (status != 0) { - return; - } - *flag = term.c_lflag; - } - - static void RestoreMode(intptr_t fd, tcflag_t flag) { - if (flag == kInvalidFlag) { - return; - } - struct termios term; - int status = TEMP_FAILURE_RETRY(tcgetattr(fd, &term)); - if (status != 0) { - return; - } - term.c_lflag = flag; - VOID_TEMP_FAILURE_RETRY(tcsetattr(fd, TCSANOW, &term)); - } - - DISALLOW_ALLOCATION(); - DISALLOW_IMPLICIT_CONSTRUCTORS(PosixConsole); -}; - -tcflag_t PosixConsole::stdout_initial_c_lflag_ = PosixConsole::kInvalidFlag; -tcflag_t PosixConsole::stderr_initial_c_lflag_ = PosixConsole::kInvalidFlag; -tcflag_t PosixConsole::stdin_initial_c_lflag_ = PosixConsole::kInvalidFlag; - void Console::SaveConfig() { - PosixConsole::Initialize(); } void Console::RestoreConfig() { - PosixConsole::Cleanup(); } } // namespace bin diff --git a/runtime/bin/console_win.cc b/runtime/bin/console_win.cc index 59e6603f32c..d714aa541d3 100644 --- a/runtime/bin/console_win.cc +++ b/runtime/bin/console_win.cc @@ -105,6 +105,10 @@ class ConsoleWin { /// to reset the state when we cleanup. if ((h != INVALID_HANDLE_VALUE) && GetConsoleMode(h, &mode)) { old_mode = mode; + // No reason to restore the mode on exit if it was already desirable. + if ((mode & flags) == flags) { + return kInvalidFlag; + } if (flags != 0) { const DWORD request = mode | flags; SetConsoleMode(h, request); diff --git a/runtime/bin/io_natives.cc b/runtime/bin/io_natives.cc index ea519356913..002cd2c2a03 100644 --- a/runtime/bin/io_natives.cc +++ b/runtime/bin/io_natives.cc @@ -176,6 +176,8 @@ namespace bin { V(Stdin_ReadByte, 1) \ V(Stdin_GetEchoMode, 1) \ V(Stdin_SetEchoMode, 2) \ + V(Stdin_GetEchoNewlineMode, 1) \ + V(Stdin_SetEchoNewlineMode, 2) \ V(Stdin_GetLineMode, 1) \ V(Stdin_SetLineMode, 2) \ V(Stdin_AnsiSupported, 1) \ diff --git a/runtime/bin/stdio.cc b/runtime/bin/stdio.cc index d833ba40456..07ea704da72 100644 --- a/runtime/bin/stdio.cc +++ b/runtime/bin/stdio.cc @@ -85,6 +85,39 @@ void FUNCTION_NAME(Stdin_SetEchoMode)(Dart_NativeArguments args) { } } +void FUNCTION_NAME(Stdin_GetEchoNewlineMode)(Dart_NativeArguments args) { + bool enabled = false; + intptr_t fd; + if (!GetIntptrArgument(args, 0, &fd)) { + return; + } + if (Stdin::GetEchoNewlineMode(fd, &enabled)) { + Dart_SetBooleanReturnValue(args, enabled); + } else { + Dart_SetReturnValue(args, DartUtils::NewDartOSError()); + } +} + +void FUNCTION_NAME(Stdin_SetEchoNewlineMode)(Dart_NativeArguments args) { + intptr_t fd; + if (!GetIntptrArgument(args, 0, &fd)) { + return; + } + bool enabled; + Dart_Handle status = Dart_GetNativeBooleanArgument(args, 1, &enabled); + if (Dart_IsError(status)) { + // The caller is expecting an OSError if something goes wrong. + OSError os_error(-1, "Invalid argument", OSError::kUnknown); + Dart_SetReturnValue(args, DartUtils::NewDartOSError(&os_error)); + return; + } + if (Stdin::SetEchoNewlineMode(fd, enabled)) { + Dart_SetReturnValue(args, Dart_True()); + } else { + Dart_SetReturnValue(args, DartUtils::NewDartOSError()); + } +} + void FUNCTION_NAME(Stdin_GetLineMode)(Dart_NativeArguments args) { bool enabled = false; intptr_t fd; diff --git a/runtime/bin/stdio.h b/runtime/bin/stdio.h index 3bbc019883f..e5796d5349f 100644 --- a/runtime/bin/stdio.h +++ b/runtime/bin/stdio.h @@ -20,6 +20,9 @@ class Stdin { static bool GetEchoMode(intptr_t fd, bool* enabled); static bool SetEchoMode(intptr_t fd, bool enabled); + static bool GetEchoNewlineMode(intptr_t fd, bool* enabled); + static bool SetEchoNewlineMode(intptr_t fd, bool enabled); + static bool GetLineMode(intptr_t fd, bool* enabled); static bool SetLineMode(intptr_t fd, bool enabled); diff --git a/runtime/bin/stdio_android.cc b/runtime/bin/stdio_android.cc index 1afb34fecd7..e620e1febba 100644 --- a/runtime/bin/stdio_android.cc +++ b/runtime/bin/stdio_android.cc @@ -44,9 +44,34 @@ bool Stdin::SetEchoMode(intptr_t fd, bool enabled) { return false; } if (enabled) { - term.c_lflag |= (ECHO | ECHONL); + term.c_lflag |= ECHO; } else { - term.c_lflag &= ~(ECHO | ECHONL); + term.c_lflag &= ~(ECHO); + } + status = NO_RETRY_EXPECTED(tcsetattr(fd, TCSANOW, &term)); + return (status == 0); +} + +bool Stdin::GetEchoNewlineMode(intptr_t fd, bool* enabled) { + struct termios term; + int status = NO_RETRY_EXPECTED(tcgetattr(fd, &term)); + if (status != 0) { + return false; + } + *enabled = ((term.c_lflag & ECHONL) != 0); + return true; +} + +bool Stdin::SetEchoNewlineMode(intptr_t fd, bool enabled) { + struct termios term; + int status = NO_RETRY_EXPECTED(tcgetattr(fd, &term)); + if (status != 0) { + return false; + } + if (enabled) { + term.c_lflag |= ECHONL; + } else { + term.c_lflag &= ~(ECHONL); } status = NO_RETRY_EXPECTED(tcsetattr(fd, TCSANOW, &term)); return (status == 0); diff --git a/runtime/bin/stdio_fuchsia.cc b/runtime/bin/stdio_fuchsia.cc index 2f91a9c5440..4d84b48debf 100644 --- a/runtime/bin/stdio_fuchsia.cc +++ b/runtime/bin/stdio_fuchsia.cc @@ -34,6 +34,16 @@ bool Stdin::SetEchoMode(intptr_t fd, bool enabled) { return false; } +bool Stdin::GetEchoNewlineMode(intptr_t fd, bool* enabled) { + errno = ENOSYS; + return false; +} + +bool Stdin::SetEchoNewlineMode(intptr_t fd, bool enabled) { + errno = ENOSYS; + return false; +} + bool Stdin::GetLineMode(intptr_t fd, bool* enabled) { errno = ENOSYS; return false; diff --git a/runtime/bin/stdio_linux.cc b/runtime/bin/stdio_linux.cc index 17db9bd8e95..4acddde261e 100644 --- a/runtime/bin/stdio_linux.cc +++ b/runtime/bin/stdio_linux.cc @@ -44,9 +44,34 @@ bool Stdin::SetEchoMode(intptr_t fd, bool enabled) { return false; } if (enabled) { - term.c_lflag |= (ECHO | ECHONL); + term.c_lflag |= ECHO; } else { - term.c_lflag &= ~(ECHO | ECHONL); + term.c_lflag &= ~(ECHO); + } + status = NO_RETRY_EXPECTED(tcsetattr(fd, TCSANOW, &term)); + return (status == 0); +} + +bool Stdin::GetEchoNewlineMode(intptr_t fd, bool* enabled) { + struct termios term; + int status = NO_RETRY_EXPECTED(tcgetattr(fd, &term)); + if (status != 0) { + return false; + } + *enabled = ((term.c_lflag & ECHONL) != 0); + return true; +} + +bool Stdin::SetEchoNewlineMode(intptr_t fd, bool enabled) { + struct termios term; + int status = NO_RETRY_EXPECTED(tcgetattr(fd, &term)); + if (status != 0) { + return false; + } + if (enabled) { + term.c_lflag |= ECHONL; + } else { + term.c_lflag &= ~(ECHONL); } status = NO_RETRY_EXPECTED(tcsetattr(fd, TCSANOW, &term)); return (status == 0); diff --git a/runtime/bin/stdio_macos.cc b/runtime/bin/stdio_macos.cc index 969b75dc049..202b1690c47 100644 --- a/runtime/bin/stdio_macos.cc +++ b/runtime/bin/stdio_macos.cc @@ -44,9 +44,34 @@ bool Stdin::SetEchoMode(intptr_t fd, bool enabled) { return false; } if (enabled) { - term.c_lflag |= (ECHO | ECHONL); + term.c_lflag |= ECHO; } else { - term.c_lflag &= ~(ECHO | ECHONL); + term.c_lflag &= ~(ECHO); + } + status = NO_RETRY_EXPECTED(tcsetattr(fd, TCSANOW, &term)); + return (status == 0); +} + +bool Stdin::GetEchoNewlineMode(intptr_t fd, bool* enabled) { + struct termios term; + int status = NO_RETRY_EXPECTED(tcgetattr(fd, &term)); + if (status != 0) { + return false; + } + *enabled = ((term.c_lflag & ECHONL) != 0); + return true; +} + +bool Stdin::SetEchoNewlineMode(intptr_t fd, bool enabled) { + struct termios term; + int status = NO_RETRY_EXPECTED(tcgetattr(fd, &term)); + if (status != 0) { + return false; + } + if (enabled) { + term.c_lflag |= ECHONL; + } else { + term.c_lflag &= ~(ECHONL); } status = NO_RETRY_EXPECTED(tcsetattr(fd, TCSANOW, &term)); return (status == 0); diff --git a/runtime/bin/stdio_win.cc b/runtime/bin/stdio_win.cc index dc65cbaa3b8..105b6115b12 100644 --- a/runtime/bin/stdio_win.cc +++ b/runtime/bin/stdio_win.cc @@ -56,6 +56,19 @@ bool Stdin::SetEchoMode(intptr_t fd, bool enabled) { return SetConsoleMode(h, mode); } +bool Stdin::GetEchoNewlineMode(intptr_t fd, bool* enabled) { + *enabled = false; + return true; +} + +bool Stdin::SetEchoNewlineMode(intptr_t fd, bool enabled) { + if (enabled) { + SetLastError(ERROR_NOT_CAPABLE); + return false; + } + return true; +} + bool Stdin::GetLineMode(intptr_t fd, bool* enabled) { HANDLE h = GetStdHandle(STD_INPUT_HANDLE); DWORD mode; diff --git a/sdk/lib/_internal/js_dev_runtime/patch/io_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/io_patch.dart index a70800fe135..3be8a091cde 100644 --- a/sdk/lib/_internal/js_dev_runtime/patch/io_patch.dart +++ b/sdk/lib/_internal/js_dev_runtime/patch/io_patch.dart @@ -687,6 +687,16 @@ class Stdin { throw UnsupportedError("Stdin.echoMode"); } + @patch + bool get echoNewlineMode { + throw UnsupportedError("Stdin.echoNewlineMode"); + } + + @patch + void set echoNewlineMode(bool enabled) { + throw UnsupportedError("Stdin.echoNewlineMode"); + } + @patch bool get lineMode { throw UnsupportedError("Stdin.lineMode"); diff --git a/sdk/lib/_internal/js_runtime/lib/io_patch.dart b/sdk/lib/_internal/js_runtime/lib/io_patch.dart index e22aae22b33..f795f7c9152 100644 --- a/sdk/lib/_internal/js_runtime/lib/io_patch.dart +++ b/sdk/lib/_internal/js_runtime/lib/io_patch.dart @@ -687,6 +687,16 @@ class Stdin { throw new UnsupportedError("Stdin.echoMode"); } + @patch + bool get echoNewlineMode { + throw UnsupportedError("Stdin.echoNewlineMode"); + } + + @patch + void set echoNewlineMode(bool enabled) { + throw UnsupportedError("Stdin.echoNewlineMode"); + } + @patch bool get lineMode { throw new UnsupportedError("Stdin.lineMode"); diff --git a/sdk/lib/_internal/vm/bin/stdio_patch.dart b/sdk/lib/_internal/vm/bin/stdio_patch.dart index 6a6bd2c2cdf..680bf44a50a 100644 --- a/sdk/lib/_internal/vm/bin/stdio_patch.dart +++ b/sdk/lib/_internal/vm/bin/stdio_patch.dart @@ -86,6 +86,29 @@ class Stdin { } } + @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); @@ -120,6 +143,10 @@ class Stdin { 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") diff --git a/sdk/lib/io/embedder_config.dart b/sdk/lib/io/embedder_config.dart index fd5bb24c1a8..9b934a6c9d9 100644 --- a/sdk/lib/io/embedder_config.dart +++ b/sdk/lib/io/embedder_config.dart @@ -24,6 +24,10 @@ abstract class _EmbedderConfig { @pragma('vm:entry-point') static bool _maySetEchoMode = true; + // Whether the isolate may set [Stdin.echoNewlineMode]. + @pragma('vm:entry-point') + static bool _maySetEchoNewlineMode = true; + // Whether the isolate may set [Stdin.lineMode]. @pragma('vm:entry-point') static bool _maySetLineMode = true; diff --git a/sdk/lib/io/stdio.dart b/sdk/lib/io/stdio.dart index dcaf1d840a2..3c8e355705e 100644 --- a/sdk/lib/io/stdio.dart +++ b/sdk/lib/io/stdio.dart @@ -112,14 +112,34 @@ class Stdin extends _StdStream implements Stream> { /// Whether echo mode is enabled on [stdin]. /// - /// If disabled, input from to console will not be echoed. + /// If disabled, input from the console will not be echoed. /// /// Default depends on the parent process, but is usually enabled. /// + /// On POSIX systems this mode is the `echo` local terminal mode. Before + /// Dart 2.18, it also controlled the `echonl` mode, which is now controlled + /// by [echoNewlineMode]. + /// /// On Windows this mode can only be enabled if [lineMode] is enabled as well. external bool get echoMode; external set echoMode(bool echoMode); + /// Whether echo newline mode is enabled on [stdin]. + /// + /// If enabled, newlines from the terminal will be echoed even if the regular + /// [echoMode] is disabled. This mode may require `lineMode` to be turned on + /// to have an effect. + /// + /// Default depends on the parent process, but is usually disabled. + /// + /// On POSIX systems this mode is the `echonl` local terminal mode. + /// + /// On Windows this mode cannot be set. + @Since("2.18") + external bool get echoNewlineMode; + @Since("2.18") + external set echoNewlineMode(bool echoNewlineMode); + /// Whether line mode is enabled on [stdin]. /// /// If enabled, characters are delayed until a newline character is entered. @@ -127,7 +147,10 @@ class Stdin extends _StdStream implements Stream> { /// /// Default depends on the parent process, but is usually enabled. /// - /// On Windows this mode can only be disabled if [echoMode] is disabled as well. + /// On POSIX systems this mode is the `icanon` local terminal mode. + /// + /// On Windows this mode can only be disabled if [echoMode] is disabled as + /// well. external bool get lineMode; external set lineMode(bool lineMode); diff --git a/tests/standalone/io/io_override_test.dart b/tests/standalone/io/io_override_test.dart index 4fe9c6662ff..7acca81a8d1 100644 --- a/tests/standalone/io/io_override_test.dart +++ b/tests/standalone/io/io_override_test.dart @@ -177,6 +177,7 @@ Future serverSocketBind(dynamic address, int port, class StdinMock extends Stream> implements Stdin { bool echoMode = false; + bool echoNewlineMode = false; bool lineMode = false; bool get hasTerminal => throw ""; bool get supportsAnsiEscapes => throw ""; diff --git a/tests/standalone_2/io/io_override_test.dart b/tests/standalone_2/io/io_override_test.dart index e7d9a8183d2..e99f6116576 100644 --- a/tests/standalone_2/io/io_override_test.dart +++ b/tests/standalone_2/io/io_override_test.dart @@ -177,6 +177,7 @@ Future serverSocketBind(address, int port, class StdinMock extends Stream> implements Stdin { bool echoMode = false; + bool echoNewlineMode = false; bool lineMode = false; bool get hasTerminal => throw ""; bool get supportsAnsiEscapes => throw "";