[dart:io] Move Platform.ansiSupported to {Stdin,Stdout}.supportsAnsiEscapes

On Windows, some Windows 10 builds support only ANSI output, but not
input, so these need to be separated.

I'm also improving the detection on Mac and Linux to avoid hardcoding
the result. Instead, supportsAnsiEscapes will be true if isatty() and
the TERM environment variable contains the string 'xterm'.

related #28614

R=lrn@google.com

Review-Url: https://codereview.chromium.org/2753233002 .
This commit is contained in:
Zach Anderson 2017-03-17 12:35:36 -07:00
parent 09886f491f
commit b3093ecee6
25 changed files with 276 additions and 81 deletions

View file

@ -14,7 +14,7 @@
* `dart:io`: Added functions `File.lastAccessed`, `File.lastAccessedSync`,
`File.setLastModified`, `File.setLastModifiedSync`, `File.setLastAccessed`,
and `File.setLastAccessedSync`.
* `dart:io`: Added `Platform.ansiSupported`.
* `dart:io`: Added `{Stdin,Stdout}.supportsAnsiEscapes`.
### Dart VM
* Calls to `print()` and `Stdout.write*()` now correctly print unicode

View file

@ -92,7 +92,6 @@ namespace bin {
V(Platform_Environment, 0) \
V(Platform_ExecutableArguments, 0) \
V(Platform_GetVersion, 0) \
V(Platform_AnsiSupported, 0) \
V(Process_Start, 11) \
V(Process_Wait, 5) \
V(Process_KillPid, 2) \
@ -148,7 +147,9 @@ namespace bin {
V(Stdin_SetEchoMode, 1) \
V(Stdin_GetLineMode, 0) \
V(Stdin_SetLineMode, 1) \
V(Stdin_AnsiSupported, 0) \
V(Stdout_GetTerminalSize, 1) \
V(Stdout_AnsiSupported, 1) \
V(StringToSystemEncoding, 1) \
V(SystemEncodingToString, 1) \
V(X509_Subject, 1) \

View file

@ -109,11 +109,6 @@ void FUNCTION_NAME(Platform_GetVersion)(Dart_NativeArguments args) {
Dart_SetReturnValue(args, Dart_NewStringFromCString(Dart_VersionString()));
}
void FUNCTION_NAME(Platform_AnsiSupported)(Dart_NativeArguments args) {
Dart_SetBooleanReturnValue(args, Platform::AnsiSupported());
}
} // namespace bin
} // namespace dart

View file

@ -83,8 +83,6 @@ class Platform {
static int GetScriptIndex() { return script_index_; }
static char** GetArgv() { return argv_; }
static bool AnsiSupported();
static DART_NORETURN void Exit(int exit_code);
private:

View file

@ -85,11 +85,6 @@ const char* Platform::ResolveExecutablePath() {
}
bool Platform::AnsiSupported() {
return true;
}
void Platform::Exit(int exit_code) {
exit(exit_code);
}

View file

@ -109,11 +109,6 @@ const char* Platform::ResolveExecutablePath() {
}
bool Platform::AnsiSupported() {
return true;
}
void Platform::Exit(int exit_code) {
exit(exit_code);
}

View file

@ -110,11 +110,6 @@ const char* Platform::ResolveExecutablePath() {
}
bool Platform::AnsiSupported() {
return true;
}
void Platform::Exit(int exit_code) {
exit(exit_code);
}

View file

@ -154,11 +154,6 @@ const char* Platform::ResolveExecutablePath() {
}
bool Platform::AnsiSupported() {
return true;
}
void Platform::Exit(int exit_code) {
exit(exit_code);
}

View file

@ -21,8 +21,6 @@
native "Platform_ExecutableArguments";
@patch static String _version()
native "Platform_GetVersion";
@patch static bool _ansiSupported()
native "Platform_AnsiSupported";
@patch static String _packageRoot()
=> VMLibraryHooks.packageRootString;

View file

@ -64,12 +64,6 @@ void FUNCTION_NAME(Platform_GetVersion)(Dart_NativeArguments args) {
"Platform is not supported on this platform"));
}
void FUNCTION_NAME(Platform_AnsiSupported)(Dart_NativeArguments args) {
Dart_ThrowException(DartUtils::NewInternalError(
"Platform is not supported on this platform"));
}
} // namespace bin
} // namespace dart

View file

@ -83,23 +83,18 @@ class PlatformWin {
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
ansi_supported_ = true;
// Try to set the bits for ANSI support, but swallow any failures.
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD out_mode;
if ((out != INVALID_HANDLE_VALUE) && GetConsoleMode(out, &out_mode)) {
const DWORD request = out_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
ansi_supported_ = ansi_supported_ && SetConsoleMode(out, request);
} else {
ansi_supported_ = false;
SetConsoleMode(out, request);
}
HANDLE in = GetStdHandle(STD_INPUT_HANDLE);
DWORD in_mode;
if ((in != INVALID_HANDLE_VALUE) && GetConsoleMode(in, &in_mode)) {
const DWORD request = in_mode | ENABLE_VIRTUAL_TERMINAL_INPUT;
ansi_supported_ = ansi_supported_ && SetConsoleMode(in, request);
} else {
ansi_supported_ = false;
SetConsoleMode(in, request);
}
}
@ -108,13 +103,10 @@ class PlatformWin {
RestoreConsoleLocked();
}
static bool ansi_supported() { return ansi_supported_; }
private:
static Mutex* platform_win_mutex_;
static int saved_output_cp_;
static int saved_input_cp_;
static bool ansi_supported_;
static void RestoreConsoleLocked() {
// STD_OUTPUT_HANDLE and STD_INPUT_HANDLE may have been closed or
@ -179,7 +171,6 @@ class PlatformWin {
int PlatformWin::saved_output_cp_ = -1;
int PlatformWin::saved_input_cp_ = -1;
Mutex* PlatformWin::platform_win_mutex_ = NULL;
bool PlatformWin::ansi_supported_ = false;
bool Platform::Initialize() {
PlatformWin::InitOnce();
@ -276,11 +267,6 @@ const char* Platform::ResolveExecutablePath() {
}
bool Platform::AnsiSupported() {
return PlatformWin::ansi_supported();
}
void Platform::Exit(int exit_code) {
// TODO(zra): Remove once VM shuts down cleanly.
::dart::private_flag_windows_run_tls_destructors = false;

View file

@ -69,6 +69,16 @@ void FUNCTION_NAME(Stdin_SetLineMode)(Dart_NativeArguments args) {
}
void FUNCTION_NAME(Stdin_AnsiSupported)(Dart_NativeArguments args) {
bool supported = false;
if (Stdin::AnsiSupported(&supported)) {
Dart_SetBooleanReturnValue(args, supported);
} else {
Dart_SetReturnValue(args, DartUtils::NewDartOSError());
}
}
void FUNCTION_NAME(Stdout_GetTerminalSize)(Dart_NativeArguments args) {
if (!Dart_IsInteger(Dart_GetNativeArgument(args, 0))) {
OSError os_error(-1, "Invalid argument", OSError::kUnknown);
@ -92,6 +102,26 @@ void FUNCTION_NAME(Stdout_GetTerminalSize)(Dart_NativeArguments args) {
}
}
void FUNCTION_NAME(Stdout_AnsiSupported)(Dart_NativeArguments args) {
if (!Dart_IsInteger(Dart_GetNativeArgument(args, 0))) {
OSError os_error(-1, "Invalid argument", OSError::kUnknown);
Dart_SetReturnValue(args, DartUtils::NewDartOSError(&os_error));
return;
}
intptr_t fd = DartUtils::GetIntptrValue(Dart_GetNativeArgument(args, 0));
if ((fd != 1) && (fd != 2)) {
Dart_SetReturnValue(args, Dart_NewApiError("Terminal fd must be 1 or 2"));
return;
}
bool supported = false;
if (Stdout::AnsiSupported(fd, &supported)) {
Dart_SetBooleanReturnValue(args, supported);
} else {
Dart_SetReturnValue(args, DartUtils::NewDartOSError());
}
}
} // namespace bin
} // namespace dart

View file

@ -27,6 +27,8 @@ class Stdin {
static bool GetLineMode(bool* enabled);
static bool SetLineMode(bool enabled);
static bool AnsiSupported(bool* supported);
private:
DISALLOW_ALLOCATION();
DISALLOW_IMPLICIT_CONSTRUCTORS(Stdin);
@ -36,6 +38,7 @@ class Stdin {
class Stdout {
public:
static bool GetTerminalSize(intptr_t fd, int size[2]);
static bool AnsiSupported(intptr_t fd, bool* supported);
private:
DISALLOW_ALLOCATION();

View file

@ -83,6 +83,21 @@ bool Stdin::SetLineMode(bool enabled) {
}
static bool TermHasXTerm() {
const char* term = getenv("TERM");
if (term == NULL) {
return false;
}
return strstr(term, "xterm") != NULL;
}
bool Stdin::AnsiSupported(bool* supported) {
*supported = isatty(STDIN_FILENO) && TermHasXTerm();
return true;
}
bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
struct winsize w;
int status = NO_RETRY_EXPECTED(ioctl(fd, TIOCGWINSZ, &w));
@ -94,6 +109,12 @@ bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
return false;
}
bool Stdout::AnsiSupported(intptr_t fd, bool* supported) {
*supported = isatty(fd) && TermHasXTerm();
return true;
}
} // namespace bin
} // namespace dart

View file

@ -42,11 +42,23 @@ bool Stdin::SetLineMode(bool enabled) {
}
bool Stdin::AnsiSupported(bool* supported) {
UNIMPLEMENTED();
return false;
}
bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
UNIMPLEMENTED();
return false;
}
bool Stdout::AnsiSupported(intptr_t fd, bool* supported) {
UNIMPLEMENTED();
return false;
}
} // namespace bin
} // namespace dart

View file

@ -83,6 +83,21 @@ bool Stdin::SetLineMode(bool enabled) {
}
static bool TermHasXTerm() {
const char* term = getenv("TERM");
if (term == NULL) {
return false;
}
return strstr(term, "xterm") != NULL;
}
bool Stdin::AnsiSupported(bool* supported) {
*supported = isatty(STDIN_FILENO) && TermHasXTerm();
return true;
}
bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
struct winsize w;
int status = NO_RETRY_EXPECTED(ioctl(fd, TIOCGWINSZ, &w));
@ -94,6 +109,12 @@ bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
return false;
}
bool Stdout::AnsiSupported(intptr_t fd, bool* supported) {
*supported = isatty(fd) && TermHasXTerm();
return true;
}
} // namespace bin
} // namespace dart

View file

@ -83,6 +83,21 @@ bool Stdin::SetLineMode(bool enabled) {
}
static bool TermHasXTerm() {
const char* term = getenv("TERM");
if (term == NULL) {
return false;
}
return strstr(term, "xterm") != NULL;
}
bool Stdin::AnsiSupported(bool* supported) {
*supported = isatty(STDIN_FILENO) && TermHasXTerm();
return true;
}
bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
struct winsize w;
int status = NO_RETRY_EXPECTED(ioctl(fd, TIOCGWINSZ, &w));
@ -94,6 +109,12 @@ bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
return false;
}
bool Stdout::AnsiSupported(intptr_t fd, bool* supported) {
*supported = isatty(fd) && TermHasXTerm();
return true;
}
} // namespace bin
} // namespace dart

View file

@ -83,11 +83,20 @@
}
}
@patch bool get supportsAnsiEscapes {
var result = _supportsAnsiEscapes();
if (result is OSError) {
throw new StdinException("Error determining ANSI support", result);
}
return result;
}
static _echoMode() native "Stdin_GetEchoMode";
static _setEchoMode(bool enabled) native "Stdin_SetEchoMode";
static _lineMode() native "Stdin_GetLineMode";
static _setLineMode(bool enabled) native "Stdin_SetLineMode";
static _readByte() native "Stdin_ReadByte";
static _supportsAnsiEscapes() native "Stdin_AnsiSupported";
}
@patch class Stdout {
@ -112,6 +121,16 @@
}
static _getTerminalSize(int fd) native "Stdout_GetTerminalSize";
@patch static bool _supportsAnsiEscapes(int fd) {
var result = _getAnsiSupported(fd);
if (result is! bool) {
throw new StdoutException("Error determining ANSI support", result);
}
return result;
}
static _getAnsiSupported(int fd) native "Stdout_AnsiSupported";
}

View file

@ -41,11 +41,23 @@ void FUNCTION_NAME(Stdin_SetLineMode)(Dart_NativeArguments args) {
}
void FUNCTION_NAME(Stdin_AnsiSupported)(Dart_NativeArguments args) {
Dart_ThrowException(
DartUtils::NewDartArgumentError("Stdin unsupported on this platform"));
}
void FUNCTION_NAME(Stdout_GetTerminalSize)(Dart_NativeArguments args) {
Dart_ThrowException(
DartUtils::NewDartArgumentError("Stdout unsupported on this platform"));
}
void FUNCTION_NAME(Stdout_AnsiSupported)(Dart_NativeArguments args) {
Dart_ThrowException(
DartUtils::NewDartArgumentError("Stdout unsupported on this platform"));
}
} // namespace bin
} // namespace dart

View file

@ -9,6 +9,16 @@
#include "bin/stdio.h"
// These are not always defined in the header files. See:
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
#endif
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif
namespace dart {
namespace bin {
@ -77,6 +87,23 @@ bool Stdin::SetLineMode(bool enabled) {
}
bool Stdin::AnsiSupported(bool* supported) {
ASSERT(supported != NULL);
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
if (h == INVALID_HANDLE_VALUE) {
*supported = false;
return false;
}
DWORD mode;
if (!GetConsoleMode(h, &mode)) {
*supported = false;
return false;
}
*supported = (mode & ENABLE_VIRTUAL_TERMINAL_INPUT) != 0;
return true;
}
bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
HANDLE h;
if (fd == 1) {
@ -93,6 +120,28 @@ bool Stdout::GetTerminalSize(intptr_t fd, int size[2]) {
return true;
}
bool Stdout::AnsiSupported(intptr_t fd, bool* supported) {
ASSERT(supported != NULL);
HANDLE h;
if (fd == 1) {
h = GetStdHandle(STD_OUTPUT_HANDLE);
} else {
h = GetStdHandle(STD_ERROR_HANDLE);
}
if (h == INVALID_HANDLE_VALUE) {
*supported = false;
return false;
}
DWORD mode;
if (!GetConsoleMode(h, &mode)) {
*supported = false;
return false;
}
*supported = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
return true;
}
} // namespace bin
} // namespace dart

View file

@ -250,11 +250,6 @@ class _Platform {
static String _version() {
throw new UnsupportedError("Platform._version");
}
@patch
static bool _ansiSupported() {
throw new UnsupportedError("Platform._ansiSupported");
}
}
@patch
@ -550,6 +545,11 @@ class Stdin {
void set lineMode(bool enabled) {
throw new UnsupportedError("Stdin.lineMode");
}
@patch
bool get supportsAnsiEscapes {
throw new UnsupportedError("Stdin.supportsAnsiEscapes");
}
}
@patch
@ -568,6 +568,11 @@ class Stdout {
int _terminalLines(int fd) {
throw new UnsupportedError("Stdout.terminalLines");
}
@patch
static bool _supportsAnsiEscapes(int fd) {
throw new UnsupportedError("Stdout.supportsAnsiEscapes");
}
}
@patch

View file

@ -71,7 +71,6 @@ class Platform {
static final _operatingSystem = _Platform.operatingSystem;
static final _localHostname = _Platform.localHostname;
static final _version = _Platform.version;
static final _ansiSupported = _Platform.ansiSupported;
/**
* Get the number of processors of the machine.
@ -125,14 +124,6 @@ class Platform {
*/
static final bool isFuchsia = (_operatingSystem == "fuchsia");
/**
* When stdio is connected to a terminal, whether ANSI codes are supported.
*
* This value is hard-coded to `true`, except on Windows where only more
* recent versions of Windows 10 support the codes.
*/
static final bool ansiSupported = _ansiSupported;
/**
* Get the environment for this process.
*

View file

@ -11,7 +11,6 @@ class _Platform {
external static _localHostname();
external static _executable();
external static _resolvedExecutable();
external static bool _ansiSupported();
/**
* Retrieve the entries of the process environment.
@ -46,7 +45,6 @@ class _Platform {
static int get numberOfProcessors => _numberOfProcessors();
static String get pathSeparator => _pathSeparator();
static String get operatingSystem => _operatingSystem();
static bool get ansiSupported => _ansiSupported();
static Uri script;
static String get localHostname {

View file

@ -136,6 +136,30 @@ class Stdin extends _StdStream implements Stream<List<int>> {
*/
external void set lineMode(bool enabled);
/**
* Whether connected to a terminal that supports ANSI escape sequences.
*
* Not all terminals are recognized, and not all recognized terminals can
* report whether they support ANSI escape sequences, so this value is a
* best-effort attempt at detecting the support.
*
* The actual escape sequence support may differ between terminals,
* with some terminals supporting more escape sequences than others,
* and some terminals even differing in behavior for the same escape
* sequence.
*
* The ANSI color selection is generally supported.
*
* Currently, a `TERM` environment variable containing the string `xterm`
* will be taken as evidence that ANSI escape sequences are supported.
* On Windows, only versions of Windows 10 after v.1511
* ("TH2", OS build 10586) will be detected as supporting the output of
* ANSI escape sequences, and only versions after v.1607 ("Anniversery
* Update", OS build 14393) will be detected as supporting the input of
* ANSI escape sequences.
*/
external bool get supportsAnsiEscapes;
/**
* Synchronously read a byte from stdin. This call will block until a byte is
* available.
@ -178,7 +202,7 @@ class Stdout extends _StdFileSink implements IOSink {
*/
int get terminalColumns => _terminalColumns(_fd);
/**
/*
* Get the number of lines of the terminal.
*
* If no terminal is attached to stdout, a [StdoutException] is thrown. See
@ -186,9 +210,34 @@ class Stdout extends _StdFileSink implements IOSink {
*/
int get terminalLines => _terminalLines(_fd);
/**
* Whether connected to a terminal that supports ANSI escape sequences.
*
* Not all terminals are recognized, and not all recognized terminals can
* report whether they support ANSI escape sequences, so this value is a
* best-effort attempt at detecting the support.
*
* The actual escape sequence support may differ between terminals,
* with some terminals supporting more escape sequences than others,
* and some terminals even differing in behavior for the same escape
* sequence.
*
* The ANSI color selection is generally supported.
*
* Currently, a `TERM` environment variable containing the string `xterm`
* will be taken as evidence that ANSI escape sequences are supported.
* On Windows, only versions of Windows 10 after v.1511
* ("TH2", OS build 10586) will be detected as supporting the output of
* ANSI escape sequences, and only versions after v.1607 ("Anniversery
* Update", OS build 14393) will be detected as supporting the input of
* ANSI escape sequences.
*/
bool get supportsAnsiEscapes => _supportsAnsiEscapes(_fd);
external bool _hasTerminal(int fd);
external int _terminalColumns(int fd);
external int _terminalLines(int fd);
external static bool _supportsAnsiEscapes(int fd);
/**
* Get a non-blocking `IOSink`.

View file

@ -6,18 +6,30 @@ import 'dart:io';
import "package:expect/expect.dart";
main() {
testStdout(Stdout s) {
try {
Platform.ansiSupported;
} catch (e, s) {
Expect.fail("Platform.ansiSupported threw: $e\n$s\n");
s.supportsAnsiEscapes;
} catch (e, st) {
Expect.fail("$s.supportsAnsiEscapes threw: $e\n$st\n");
}
Expect.isNotNull(Platform.ansiSupported);
Expect.isTrue(Platform.ansiSupported is bool);
if (stdout.hasTerminal && Platform.ansiSupported) {
stdout.writeln('\x1b[31mThis text has a red foreground using SGR.31.');
stdout.writeln('\x1b[39mThis text has restored the foreground color.');
Expect.isNotNull(s.supportsAnsiEscapes);
Expect.isTrue(s.supportsAnsiEscapes is bool);
if (s.supportsAnsiEscapes) {
s.writeln('\x1b[31mThis text has a red foreground using SGR.31.');
s.writeln('\x1b[39mThis text has restored the foreground color.');
} else {
stdout.writeln('ANSI codes not supported on this platform');
s.writeln('ANSI escape codes are not supported on this platform');
}
}
main() {
testStdout(stdout);
testStdout(stderr);
try {
stdin.supportsAnsiEscapes;
} catch (e, st) {
Expect.fail("stdin.supportsAnsiEscapes threw: $e\n$st\n");
}
Expect.isNotNull(stdin.supportsAnsiEscapes);
Expect.isTrue(stdin.supportsAnsiEscapes is bool);
}