| // 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. |
| |
| 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: SYSTEM_ENCODING, |
| Encoding stderrEncoding: SYSTEM_ENCODING}) { |
| 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: SYSTEM_ENCODING, |
| Encoding stderrEncoding: SYSTEM_ENCODING}) { |
| return _runNonInteractiveProcessSync(executable, |
| arguments, |
| workingDirectory, |
| environment, |
| includeParentEnvironment, |
| runInShell, |
| stdoutEncoding, |
| stderrEncoding); |
| } |
| |
| /* patch */ static bool killPid( |
| int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) { |
| if (signal is! ProcessSignal) { |
| throw new ArgumentError( |
| "Argument 'signal' must be a ProcessSignal"); |
| } |
| return _ProcessUtils._killPid(pid, signal._signalNumber); |
| } |
| } |
| |
| |
| List<_SignalController> _signalControllers = new List(32); |
| |
| |
| class _SignalController { |
| final ProcessSignal signal; |
| |
| StreamController _controller; |
| var _id; |
| |
| _SignalController(this.signal) { |
| _controller = new StreamController.broadcast( |
| 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.watch(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; |
| } |
| } |
| |
| /* patch */ static _setSignalHandler(int signal) |
| native "Process_SetSignalHandler"; |
| /* patch */ static int _clearSignalHandler(int signal) |
| native "Process_ClearSignalHandler"; |
| } |
| |
| 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; |
| } |
| } |
| |
| |
| class _ProcessStartStatus { |
| int _errorCode; // Set to OS error code if process start failed. |
| 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 { |
| _ProcessResourceInfo _resourceInfo; |
| static bool connectedResourceHandler = false; |
| |
| _ProcessImpl(String path, |
| List<String> arguments, |
| String this._workingDirectory, |
| Map<String, String> environment, |
| bool includeParentEnvironment, |
| bool runInShell, |
| ProcessStartMode mode) : super() { |
| if (!connectedResourceHandler) { |
| registerExtension('__getProcesses', |
| _ProcessResourceInfo.getStartedProcesses); |
| registerExtension('__getProcessById', |
| _ProcessResourceInfo.getProcessInfoMapById); |
| connectedResourceHandler = true; |
| } |
| |
| if (runInShell) { |
| arguments = _getShellArguments(path, arguments); |
| path = _getShellCommand(); |
| } |
| |
| if (path is !String) { |
| throw new ArgumentError("Path is not a String: $path"); |
| } |
| _path = path; |
| |
| if (arguments is !List) { |
| throw new ArgumentError("Arguments is not a List: $arguments"); |
| } |
| int len = arguments.length; |
| _arguments = new List<String>(len); |
| for (int i = 0; i < len; i++) { |
| var arg = arguments[i]; |
| if (arg is !String) { |
| throw new ArgumentError("Non-string argument: $arg"); |
| } |
| _arguments[i] = arguments[i]; |
| if (Platform.isWindows) { |
| _arguments[i] = _windowsArgumentEscape(_arguments[i]); |
| } |
| } |
| |
| if (_workingDirectory != null && _workingDirectory is !String) { |
| throw new ArgumentError( |
| "WorkingDirectory is not a String: $_workingDirectory"); |
| } |
| |
| _environment = []; |
| // Ensure that we have a non-null environment. |
| environment = (environment == null) ? (const {}) : environment; |
| if (environment is !Map) { |
| throw new ArgumentError("Environment is not a map: $environment"); |
| } |
| environment.forEach((key, value) { |
| if (key is !String || value is !String) { |
| throw new ArgumentError( |
| "Environment key or value is not a string: ($key, $value)"); |
| } |
| _environment.add('$key=$value'); |
| }); |
| if (includeParentEnvironment) { |
| Platform.environment.forEach((key, value) { |
| assert(key is String); |
| assert(value is String); |
| // Do not override keys already set as part of environment. |
| if (!environment.containsKey(key)) { |
| _environment.add('$key=$value'); |
| } |
| }); |
| } |
| |
| if (mode is !ProcessStartMode) { |
| throw new ArgumentError("Mode is not a ProcessStartMode: $mode"); |
| } |
| _mode = mode; |
| |
| if (mode != ProcessStartMode.DETACHED) { |
| // stdin going to process. |
| _stdin = new _StdSink(new _Socket._writePipe()); |
| _stdin._sink._owner = this; |
| // stdout coming from process. |
| _stdout = new _StdStream(new _Socket._readPipe()); |
| _stdout._stream._owner = this; |
| // stderr coming from process. |
| _stderr = new _StdStream(new _Socket._readPipe()); |
| _stderr._stream._owner = this; |
| } |
| if (mode == ProcessStartMode.NORMAL) { |
| _exitHandler = new _Socket._readPipe(); |
| } |
| _ended = false; |
| _started = false; |
| } |
| |
| 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) { |
| 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(); |
| if (_mode == ProcessStartMode.NORMAL) { |
| _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(_path, |
| _arguments, |
| _workingDirectory, |
| _environment, |
| _mode.index, |
| _mode == ProcessStartMode.DETACHED |
| ? null : _stdin._sink._nativeSocket, |
| _mode == ProcessStartMode.DETACHED |
| ? null : _stdout._stream._nativeSocket, |
| _mode == ProcessStartMode.DETACHED |
| ? null : _stderr._stream._nativeSocket, |
| _mode != ProcessStartMode.NORMAL |
| ? null : _exitHandler._nativeSocket, |
| status); |
| if (!success) { |
| completer.completeError( |
| new ProcessException(_path, |
| _arguments, |
| status._errorMessage, |
| status._errorCode)); |
| return; |
| } |
| |
| _started = true; |
| _resourceInfo = new _ProcessResourceInfo(this); |
| |
| // Setup an exit handler to handle internal cleanup and possible |
| // callback when a process terminates. |
| if (_mode == ProcessStartMode.NORMAL) { |
| int exitDataRead = 0; |
| final int EXIT_DATA_SIZE = 8; |
| List<int> exitDataBuffer = new List<int>(EXIT_DATA_SIZE); |
| _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. |
| _stdin._sink.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(_path, |
| _arguments, |
| _workingDirectory, |
| _environment, |
| ProcessStartMode.NORMAL.index, |
| _stdin._sink._nativeSocket, |
| _stdout._stream._nativeSocket, |
| _stderr._stream._nativeSocket, |
| _exitHandler._nativeSocket, |
| status); |
| if (!success) { |
| throw new ProcessException(_path, |
| _arguments, |
| status._errorMessage, |
| status._errorCode); |
| } |
| |
| _resourceInfo = new _ProcessResourceInfo(this); |
| |
| var result = _wait( |
| _stdin._sink._nativeSocket, |
| _stdout._stream._nativeSocket, |
| _stderr._stream._nativeSocket, |
| _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(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 { |
| return _stdout; |
| } |
| |
| Stream<List<int>> get stderr { |
| return _stderr; |
| } |
| |
| IOSink get stdin { |
| return _stdin; |
| } |
| |
| Future<int> get exitCode => _exitCode != null ? _exitCode.future : null; |
| |
| bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) { |
| if (signal is! ProcessSignal) { |
| throw new ArgumentError( |
| "Argument 'signal' must be a ProcessSignal"); |
| } |
| assert(_started); |
| if (_ended) return false; |
| return _ProcessUtils._killPid(pid, signal._signalNumber); |
| } |
| |
| int get pid => _ProcessUtils._pid(this); |
| |
| String _path; |
| List<String> _arguments; |
| String _workingDirectory; |
| List<String> _environment; |
| ProcessStartMode _mode; |
| // Private methods of Socket are used by _in, _out, and _err. |
| _StdSink _stdin; |
| _StdStream _stdout; |
| _StdStream _stderr; |
| Socket _exitHandler; |
| bool _ended; |
| bool _started; |
| 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(new BytesBuilder(), (builder, data) => builder..add(data)) |
| .then((builder) => builder.takeBytes()); |
| } else { |
| return stream |
| .transform(encoding.decoder) |
| .fold( |
| 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); |
| } |