| // 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 |
| static String _decodeBytes(List<int> bytes) native "SystemEncodingToString"; |
| } |
| |
| @patch |
| class _WindowsCodePageEncoder { |
| @patch |
| static List<int> _encodeString(String string) native "StringToSystemEncoding"; |
| } |
| |
| @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; |
| } |
| } |
| |
| static _setSignalHandler(int signal) native "Process_SetSignalHandler"; |
| static void _clearSignalHandler(int signal) |
| native "Process_ClearSignalHandler"; |
| } |
| |
| @pragma("vm:entry-point", "call") |
| Function _getWatchSignalInternal() => _ProcessUtils._watchSignalInternal; |
| |
| @patch |
| class _ProcessUtils { |
| @patch |
| static void _exit(int status) native "Process_Exit"; |
| @patch |
| static void _setExitCode(int status) native "Process_SetExitCode"; |
| @patch |
| static int _getExitCode() native "Process_GetExitCode"; |
| @patch |
| static void _sleep(int millis) native "Process_Sleep"; |
| @patch |
| static int _pid(Process? process) native "Process_Pid"; |
| static bool _killPid(int pid, int signal) native "Process_KillPid"; |
| @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; |
| } |
| |
| static _maxRss() native "ProcessInfo_MaxRSS"; |
| static _currentRss() native "ProcessInfo_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.getProcesses', _ProcessResourceInfo.getStartedProcesses); |
| registerExtension('ext.dart.io.getProcessById', |
| _ProcessResourceInfo.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>(); |
| 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!)); |
| return; |
| } |
| |
| _started = true; |
| final resourceInfo = new _ProcessResourceInfo(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 _ProcessResourceInfo(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)); |
| } |
| |
| 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) native "Process_Start"; |
| |
| _wait(_NativeSocket? stdin, _NativeSocket? stdout, _NativeSocket? stderr, |
| _NativeSocket exitHandler) native "Process_Wait"; |
| |
| 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); |
| } |