// 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 bytes) native "SystemEncodingToString"; } @patch class _WindowsCodePageEncoder { @patch static List _encodeString(String string) native "StringToSystemEncoding"; } @patch class Process { @patch static Future start(String executable, List arguments, {String workingDirectory, Map 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 run(String executable, List arguments, {String workingDirectory, Map 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 arguments, {String workingDirectory, Map 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 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; } } static _setSignalHandler(int signal) native "Process_SetSignalHandler"; 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 _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 _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 arguments, this._workingDirectory, Map environment, bool includeParentEnvironment, bool runInShell, ProcessStartMode mode) : super() { 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 (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(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 _getShellArguments( String executable, List arguments) { List 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 bytes, int offset) { return (bytes[offset] + (bytes[offset + 1] << 8) + (bytes[offset + 2] << 16) + (bytes[offset + 3] << 24)); } Future _start() { var completer = new Completer(); if (_mode == ProcessStartMode.NORMAL) { _exitCode = new Completer(); } // 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 exitDataBuffer = new List(EXIT_DATA_SIZE); _exitHandler.listen((data) { int exitCode(List 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(); 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 arguments, String workingDirectory, List 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> get stdout { return _stdout; } Stream> get stderr { return _stderr; } IOSink get stdin { return _stdin; } Future 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 _arguments; String _workingDirectory; List _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 _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 _runNonInteractiveProcess( String path, List arguments, String workingDirectory, Map 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> 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 arguments, String workingDirectory, Map 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); }