blob: 426f9d9d245a54485e3650ed1410633382d793cf [file] [log] [blame]
// 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 _WindowsCodePageDecoder {
@patch
@pragma("vm:external-name", "SystemEncodingToString")
external static String _decodeBytes(List<int> bytes);
}
@patch
class _WindowsCodePageEncoder {
@patch
@pragma("vm:external-name", "StringToSystemEncoding")
external static List<int> _encodeString(String string);
}
@patch
class Process {
@patch
static Future<Process> start(String executable, List<String> arguments,
{String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment: true,
bool runInShell: false,
ProcessStartMode mode: ProcessStartMode.normal}) {
_ProcessImpl process = new _ProcessImpl(
executable,
arguments,
workingDirectory,
environment,
includeParentEnvironment,
runInShell,
mode);
return process._start();
}
@patch
static Future<ProcessResult> run(String executable, List<String> arguments,
{String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment: true,
bool runInShell: false,
Encoding? stdoutEncoding: systemEncoding,
Encoding? stderrEncoding: systemEncoding}) {
return _runNonInteractiveProcess(
executable,
arguments,
workingDirectory,
environment,
includeParentEnvironment,
runInShell,
stdoutEncoding,
stderrEncoding);
}
@patch
static ProcessResult runSync(String executable, List<String> arguments,
{String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment: true,
bool runInShell: false,
Encoding? stdoutEncoding: systemEncoding,
Encoding? stderrEncoding: systemEncoding}) {
return _runNonInteractiveProcessSync(
executable,
arguments,
workingDirectory,
environment,
includeParentEnvironment,
runInShell,
stdoutEncoding,
stderrEncoding);
}
@patch
static bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) {
// TODO(40614): Remove once non-nullability is sound.
ArgumentError.checkNotNull(signal, "signal");
return _ProcessUtils._killPid(pid, signal._signalNumber);
}
}
List<_SignalController?> _signalControllers = new List.filled(32, null);
class _SignalController {
final ProcessSignal signal;
final _controller = new StreamController<ProcessSignal>.broadcast();
var _id;
_SignalController(this.signal) {
_controller
..onListen = _listen
..onCancel = _cancel;
}
Stream<ProcessSignal> get stream => _controller.stream;
void _listen() {
var id = _setSignalHandler(signal._signalNumber);
if (id is! int) {
_controller
.addError(new SignalException("Failed to listen for $signal", id));
return;
}
_id = id;
var socket = new _RawSocket(new _NativeSocket.watchSignal(id));
socket.listen((event) {
if (event == RawSocketEvent.read) {
var bytes = socket.read()!;
for (int i = 0; i < bytes.length; i++) {
_controller.add(signal);
}
}
});
}
void _cancel() {
if (_id != null) {
_clearSignalHandler(signal._signalNumber);
_id = null;
}
}
@pragma("vm:external-name", "Process_SetSignalHandler")
external static _setSignalHandler(int signal);
@pragma("vm:external-name", "Process_ClearSignalHandler")
external static void _clearSignalHandler(int signal);
}
@pragma("vm:entry-point", "call")
Function _getWatchSignalInternal() => _ProcessUtils._watchSignalInternal;
@patch
class _ProcessUtils {
@patch
@pragma("vm:external-name", "Process_Exit")
external static Never _exit(int status);
@patch
@pragma("vm:external-name", "Process_SetExitCode")
external static void _setExitCode(int status);
@patch
@pragma("vm:external-name", "Process_GetExitCode")
external static int _getExitCode();
@patch
@pragma("vm:external-name", "Process_Sleep")
external static void _sleep(int millis);
@patch
@pragma("vm:external-name", "Process_Pid")
external static int _pid(Process? process);
@pragma("vm:external-name", "Process_KillPid")
external static bool _killPid(int pid, int signal);
@patch
static Stream<ProcessSignal> _watchSignal(ProcessSignal signal) {
if (signal != ProcessSignal.sighup &&
signal != ProcessSignal.sigint &&
signal != ProcessSignal.sigterm &&
(Platform.isWindows ||
(signal != ProcessSignal.sigusr1 &&
signal != ProcessSignal.sigusr2 &&
signal != ProcessSignal.sigwinch))) {
throw new SignalException(
"Listening for signal $signal is not supported");
}
return _watchSignalInternal(signal);
}
static Stream<ProcessSignal> _watchSignalInternal(ProcessSignal signal) {
if (_signalControllers[signal._signalNumber] == null) {
_signalControllers[signal._signalNumber] = new _SignalController(signal);
}
return _signalControllers[signal._signalNumber]!.stream;
}
}
@patch
class ProcessInfo {
@patch
static int get maxRss {
var result = _maxRss();
if (result is OSError) {
throw result;
}
return result;
}
@patch
static int get currentRss {
var result = _currentRss();
if (result is OSError) {
throw result;
}
return result;
}
@pragma("vm:external-name", "ProcessInfo_MaxRSS")
external static _maxRss();
@pragma("vm:external-name", "ProcessInfo_CurrentRSS")
external static _currentRss();
}
@pragma("vm:entry-point")
class _ProcessStartStatus {
@pragma("vm:entry-point", "set")
int? _errorCode; // Set to OS error code if process start failed.
@pragma("vm:entry-point", "set")
String? _errorMessage; // Set to OS error message if process start failed.
}
// The NativeFieldWrapperClass1 can not be used with a mixin, due to missing
// implicit constructor.
class _ProcessImplNativeWrapper extends NativeFieldWrapperClass1 {}
class _ProcessImpl extends _ProcessImplNativeWrapper implements Process {
static bool connectedResourceHandler = false;
_ProcessImpl(
String path,
List<String> arguments,
this._workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment,
bool runInShell,
this._mode)
: super() {
// TODO(40614): Remove once non-nullability is sound.
ArgumentError.checkNotNull(path, "path");
ArgumentError.checkNotNull(arguments, "arguments");
for (int i = 0; i < arguments.length; i++) {
ArgumentError.checkNotNull(arguments[i], "arguments[]");
}
ArgumentError.checkNotNull(_mode, "mode");
if (!connectedResourceHandler) {
registerExtension('ext.dart.io.getSpawnedProcesses',
_SpawnedProcessResourceInfo.getStartedProcesses);
registerExtension('ext.dart.io.getSpawnedProcessById',
_SpawnedProcessResourceInfo.getProcessInfoMapById);
connectedResourceHandler = true;
}
if (runInShell) {
arguments = _getShellArguments(path, arguments);
path = _getShellCommand();
}
if (Platform.isWindows && path.contains(' ') && !path.contains('"')) {
// Escape paths that may contain spaces
// Bug: https://github.com/dart-lang/sdk/issues/37751
_path = '"$path"';
} else {
_path = path;
}
_arguments = [
for (int i = 0; i < arguments.length; i++)
Platform.isWindows
? _windowsArgumentEscape(arguments[i])
: arguments[i],
];
_environment = [];
// Ensure that we have a non-null environment.
environment ??= const {};
environment.forEach((key, value) {
_environment.add('$key=$value');
});
if (includeParentEnvironment) {
Platform.environment.forEach((key, value) {
// Do not override keys already set as part of environment.
if (!environment!.containsKey(key)) {
_environment.add('$key=$value');
}
});
}
if (_modeHasStdio(_mode)) {
// stdin going to process.
_stdin = new _StdSink(new _Socket._writePipe().._owner = this);
// stdout coming from process.
_stdout = new _StdStream(new _Socket._readPipe().._owner = this);
// stderr coming from process.
_stderr = new _StdStream(new _Socket._readPipe().._owner = this);
}
if (_modeIsAttached(_mode)) {
_exitHandler = new _Socket._readPipe();
}
}
_NativeSocket get _stdinNativeSocket =>
(_stdin!._sink as _Socket)._nativeSocket;
_NativeSocket get _stdoutNativeSocket =>
(_stdout!._stream as _Socket)._nativeSocket;
_NativeSocket get _stderrNativeSocket =>
(_stderr!._stream as _Socket)._nativeSocket;
static bool _modeIsAttached(ProcessStartMode mode) {
return (mode == ProcessStartMode.normal) ||
(mode == ProcessStartMode.inheritStdio);
}
static bool _modeHasStdio(ProcessStartMode mode) {
return (mode == ProcessStartMode.normal) ||
(mode == ProcessStartMode.detachedWithStdio);
}
static String _getShellCommand() {
if (Platform.isWindows) {
return 'cmd.exe';
}
return '/bin/sh';
}
static List<String> _getShellArguments(
String executable, List<String> arguments) {
List<String> shellArguments = [];
if (Platform.isWindows) {
shellArguments.add('/c');
shellArguments.add(executable);
for (var arg in arguments) {
shellArguments.add(arg);
}
} else {
var commandLine = new StringBuffer();
executable = executable.replaceAll("'", "'\"'\"'");
commandLine.write("'$executable'");
shellArguments.add("-c");
for (var arg in arguments) {
arg = arg.replaceAll("'", "'\"'\"'");
commandLine.write(" '$arg'");
}
shellArguments.add(commandLine.toString());
}
return shellArguments;
}
String _windowsArgumentEscape(String argument) {
if (argument.isEmpty) {
return '""';
}
var result = argument;
if (argument.contains('\t') ||
argument.contains(' ') ||
argument.contains('"')) {
// Produce something that the C runtime on Windows will parse
// back as this string.
// Replace any number of '\' followed by '"' with
// twice as many '\' followed by '\"'.
var backslash = '\\'.codeUnitAt(0);
var sb = new StringBuffer();
var nextPos = 0;
var quotePos = argument.indexOf('"', nextPos);
while (quotePos != -1) {
var numBackslash = 0;
var pos = quotePos - 1;
while (pos >= 0 && argument.codeUnitAt(pos) == backslash) {
numBackslash++;
pos--;
}
sb.write(argument.substring(nextPos, quotePos - numBackslash));
for (var i = 0; i < numBackslash; i++) {
sb.write(r'\\');
}
sb.write(r'\"');
nextPos = quotePos + 1;
quotePos = argument.indexOf('"', nextPos);
}
sb.write(argument.substring(nextPos, argument.length));
result = sb.toString();
// Add '"' at the beginning and end and replace all '\' at
// the end with two '\'.
sb = new StringBuffer('"');
sb.write(result);
nextPos = argument.length - 1;
while (argument.codeUnitAt(nextPos) == backslash) {
sb.write('\\');
nextPos--;
}
sb.write('"');
result = sb.toString();
}
return result;
}
int _intFromBytes(List<int> bytes, int offset) {
return (bytes[offset] +
(bytes[offset + 1] << 8) +
(bytes[offset + 2] << 16) +
(bytes[offset + 3] << 24));
}
Future<Process> _start() {
var completer = new Completer<Process>();
var stackTrace = StackTrace.current;
if (_modeIsAttached(_mode)) {
_exitCode = new Completer<int>();
}
// TODO(ager): Make the actual process starting really async instead of
// simulating it with a timer.
Timer.run(() {
var status = new _ProcessStartStatus();
bool success = _startNative(
_Namespace._namespace,
_path,
_arguments,
_workingDirectory,
_environment,
_mode._mode,
_modeHasStdio(_mode) ? _stdinNativeSocket : null,
_modeHasStdio(_mode) ? _stdoutNativeSocket : null,
_modeHasStdio(_mode) ? _stderrNativeSocket : null,
_modeIsAttached(_mode) ? _exitHandler._nativeSocket : null,
status);
if (!success) {
completer.completeError(
new ProcessException(
_path, _arguments, status._errorMessage!, status._errorCode!),
stackTrace);
return;
}
_started = true;
final resourceInfo = new _SpawnedProcessResourceInfo(this);
// Setup an exit handler to handle internal cleanup and possible
// callback when a process terminates.
if (_modeIsAttached(_mode)) {
int exitDataRead = 0;
final int EXIT_DATA_SIZE = 8;
List<int> exitDataBuffer = new List<int>.filled(EXIT_DATA_SIZE, 0);
_exitHandler.listen((data) {
int exitCode(List<int> ints) {
var code = _intFromBytes(ints, 0);
var negative = _intFromBytes(ints, 4);
assert(negative == 0 || negative == 1);
return (negative == 0) ? code : -code;
}
void handleExit() {
_ended = true;
_exitCode!.complete(exitCode(exitDataBuffer));
// Kill stdin, helping hand if the user forgot to do it.
if (_modeHasStdio(_mode)) {
(_stdin!._sink as _Socket).destroy();
}
resourceInfo.stopped();
}
exitDataBuffer.setRange(
exitDataRead, exitDataRead + data.length, data);
exitDataRead += data.length;
if (exitDataRead == EXIT_DATA_SIZE) {
handleExit();
}
});
}
completer.complete(this);
});
return completer.future;
}
ProcessResult _runAndWait(
Encoding? stdoutEncoding, Encoding? stderrEncoding) {
var status = new _ProcessStartStatus();
_exitCode = new Completer<int>();
bool success = _startNative(
_Namespace._namespace,
_path,
_arguments,
_workingDirectory,
_environment,
ProcessStartMode.normal._mode,
_stdinNativeSocket,
_stdoutNativeSocket,
_stderrNativeSocket,
_exitHandler._nativeSocket,
status);
if (!success) {
throw new ProcessException(
_path, _arguments, status._errorMessage!, status._errorCode!);
}
final resourceInfo = new _SpawnedProcessResourceInfo(this);
var result = _wait(_stdinNativeSocket, _stdoutNativeSocket,
_stderrNativeSocket, _exitHandler._nativeSocket);
getOutput(output, encoding) {
if (encoding == null) return output;
return encoding.decode(output);
}
resourceInfo.stopped();
return new ProcessResult(
result[0],
result[1],
getOutput(result[2], stdoutEncoding),
getOutput(result[3], stderrEncoding));
}
@pragma("vm:external-name", "Process_Start")
external bool _startNative(
_Namespace namespace,
String path,
List<String> arguments,
String? workingDirectory,
List<String> environment,
int mode,
_NativeSocket? stdin,
_NativeSocket? stdout,
_NativeSocket? stderr,
_NativeSocket? exitHandler,
_ProcessStartStatus status);
@pragma("vm:external-name", "Process_Wait")
external _wait(_NativeSocket? stdin, _NativeSocket? stdout,
_NativeSocket? stderr, _NativeSocket exitHandler);
Stream<List<int>> get stdout =>
_stdout ?? (throw StateError("stdio is not connected"));
Stream<List<int>> get stderr =>
_stderr ?? (throw StateError("stdio is not connected"));
IOSink get stdin => _stdin ?? (throw StateError("stdio is not connected"));
Future<int> get exitCode =>
_exitCode?.future ?? (throw StateError("Process is detached"));
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
// TODO(40614): Remove once non-nullability is sound.
ArgumentError.checkNotNull(kill, "kill");
assert(_started);
if (_ended) return false;
return _ProcessUtils._killPid(pid, signal._signalNumber);
}
int get pid => _ProcessUtils._pid(this);
late String _path;
late List<String> _arguments;
String? _workingDirectory;
late List<String> _environment;
final ProcessStartMode _mode;
// Private methods of Socket are used by _in, _out, and _err.
_StdSink? _stdin;
_StdStream? _stdout;
_StdStream? _stderr;
late _Socket _exitHandler;
bool _ended = false;
bool _started = false;
Completer<int>? _exitCode;
}
// _NonInteractiveProcess is a wrapper around an interactive process
// that buffers output so it can be delivered when the process exits.
// _NonInteractiveProcess is used to implement the Process.run
// method.
Future<ProcessResult> _runNonInteractiveProcess(
String path,
List<String> arguments,
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment,
bool runInShell,
Encoding? stdoutEncoding,
Encoding? stderrEncoding) {
// Start the underlying process.
return Process.start(path, arguments,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell)
.then((Process p) {
int pid = p.pid;
// Make sure the process stdin is closed.
p.stdin.close();
// Setup stdout and stderr handling.
Future foldStream(Stream<List<int>> stream, Encoding? encoding) {
if (encoding == null) {
return stream
.fold<BytesBuilder>(
new BytesBuilder(), (builder, data) => builder..add(data))
.then((builder) => builder.takeBytes());
} else {
return stream
.transform(encoding.decoder)
.fold<StringBuffer>(new StringBuffer(), (buf, data) {
buf.write(data);
return buf;
}).then((sb) => sb.toString());
}
}
Future stdout = foldStream(p.stdout, stdoutEncoding);
Future stderr = foldStream(p.stderr, stderrEncoding);
return Future.wait([p.exitCode, stdout, stderr]).then((result) {
return new ProcessResult(pid, result[0], result[1], result[2]);
});
});
}
ProcessResult _runNonInteractiveProcessSync(
String executable,
List<String> arguments,
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment,
bool runInShell,
Encoding? stdoutEncoding,
Encoding? stderrEncoding) {
var process = new _ProcessImpl(
executable,
arguments,
workingDirectory,
environment,
includeParentEnvironment,
runInShell,
ProcessStartMode.normal);
return process._runAndWait(stdoutEncoding, stderrEncoding);
}