OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 _exit(int status) native "Exit"; | |
6 | |
7 class _ProcessStartStatus { | |
8 int _errorCode; // Set to OS error code if process start failed. | |
9 String _errorMessage; // Set to OS error message if process start failed. | |
10 } | |
11 | |
12 | |
13 class _Process extends NativeFieldWrapperClass1 implements Process { | |
14 static Future<ProcessResult> run(String path, | |
15 List<String> arguments, | |
16 [ProcessOptions options]) { | |
17 return new _NonInteractiveProcess(path, arguments, options)._result; | |
18 } | |
19 | |
20 static Future<Process> start(String path, | |
21 List<String> arguments, | |
22 ProcessOptions options) { | |
23 _Process process = new _Process(path, arguments, options); | |
24 return process._start(); | |
25 } | |
26 | |
27 _Process(String path, List<String> arguments, ProcessOptions options) { | |
28 if (path is !String) { | |
29 throw new ArgumentError("Path is not a String: $path"); | |
30 } | |
31 _path = path; | |
32 | |
33 if (arguments is !List) { | |
34 throw new ArgumentError("Arguments is not a List: $arguments"); | |
35 } | |
36 int len = arguments.length; | |
37 _arguments = new List<String>(len); | |
38 for (int i = 0; i < len; i++) { | |
39 var arg = arguments[i]; | |
40 if (arg is !String) { | |
41 throw new ArgumentError("Non-string argument: $arg"); | |
42 } | |
43 _arguments[i] = arguments[i]; | |
44 if (Platform.operatingSystem == 'windows') { | |
45 _arguments[i] = _windowsArgumentEscape(_arguments[i]); | |
46 } | |
47 } | |
48 | |
49 if (options !== null && options.workingDirectory !== null) { | |
50 _workingDirectory = options.workingDirectory; | |
51 if (_workingDirectory is !String) { | |
52 throw new ArgumentError( | |
53 "WorkingDirectory is not a String: $_workingDirectory"); | |
54 } | |
55 } | |
56 | |
57 if (options !== null && options.environment !== null) { | |
58 var env = options.environment; | |
59 if (env is !Map) { | |
60 throw new ArgumentError("Environment is not a map: $env"); | |
61 } | |
62 _environment = []; | |
63 env.forEach((key, value) { | |
64 if (key is !String || value is !String) { | |
65 throw new ArgumentError( | |
66 "Environment key or value is not a string: ($key, $value)"); | |
67 } | |
68 _environment.add('$key=$value'); | |
69 }); | |
70 } | |
71 | |
72 _in = new _Socket._internalReadOnly(); // stdout coming from process. | |
73 _out = new _Socket._internalWriteOnly(); // stdin going to process. | |
74 _err = new _Socket._internalReadOnly(); // stderr coming from process. | |
75 _exitHandler = new _Socket._internalReadOnly(); | |
76 _ended = false; | |
77 _started = false; | |
78 _onExit = null; | |
79 } | |
80 | |
81 String _windowsArgumentEscape(String argument) { | |
82 var result = argument; | |
83 if (argument.contains('\t') || argument.contains(' ')) { | |
84 // Produce something that the C runtime on Windows will parse | |
85 // back as this string. | |
86 | |
87 // Replace any number of '\' followed by '"' with | |
88 // twice as many '\' followed by '\"'. | |
89 var backslash = '\\'.charCodeAt(0); | |
90 var sb = new StringBuffer(); | |
91 var nextPos = 0; | |
92 var quotePos = argument.indexOf('"', nextPos); | |
93 while (quotePos != -1) { | |
94 var numBackslash = 0; | |
95 var pos = quotePos - 1; | |
96 while (pos >= 0 && argument.charCodeAt(pos) == backslash) { | |
97 numBackslash++; | |
98 pos--; | |
99 } | |
100 sb.add(argument.substring(nextPos, quotePos - numBackslash)); | |
101 for (var i = 0; i < numBackslash; i++) { | |
102 sb.add(r'\\'); | |
103 } | |
104 sb.add(r'\"'); | |
105 nextPos = quotePos + 1; | |
106 quotePos = argument.indexOf('"', nextPos); | |
107 } | |
108 sb.add(argument.substring(nextPos, argument.length)); | |
109 result = sb.toString(); | |
110 | |
111 // Add '"' at the beginning and end and replace all '\' at | |
112 // the end with two '\'. | |
113 sb = new StringBuffer('"'); | |
114 sb.add(result); | |
115 nextPos = argument.length - 1; | |
116 while (argument.charCodeAt(nextPos) == backslash) { | |
117 sb.add('\\'); | |
118 nextPos--; | |
119 } | |
120 sb.add('"'); | |
121 result = sb.toString(); | |
122 } | |
123 | |
124 return result; | |
125 } | |
126 | |
127 int _intFromBytes(List<int> bytes, int offset) { | |
128 return (bytes[offset] + | |
129 (bytes[offset + 1] << 8) + | |
130 (bytes[offset + 2] << 16) + | |
131 (bytes[offset + 3] << 24)); | |
132 } | |
133 | |
134 Future<Process> _start() { | |
135 var completer = new Completer(); | |
136 // TODO(ager): Make the actual process starting really async instead of | |
137 // simulating it with a timer. | |
138 new Timer(0, (_) { | |
139 var status = new _ProcessStartStatus(); | |
140 bool success = _startNative(_path, | |
141 _arguments, | |
142 _workingDirectory, | |
143 _environment, | |
144 _in, | |
145 _out, | |
146 _err, | |
147 _exitHandler, | |
148 status); | |
149 if (!success) { | |
150 _in.close(); | |
151 _out.close(); | |
152 _err.close(); | |
153 _exitHandler.close(); | |
154 completer.completeException( | |
155 new ProcessException(status._errorMessage, status._errorCode)); | |
156 return; | |
157 } | |
158 _started = true; | |
159 | |
160 _in._closed = false; | |
161 _out._closed = false; | |
162 _err._closed = false; | |
163 _exitHandler._closed = false; | |
164 | |
165 // Make sure to activate socket handlers now that the file | |
166 // descriptors have been set. | |
167 _in._activateHandlers(); | |
168 _out._activateHandlers(); | |
169 _err._activateHandlers(); | |
170 | |
171 // Setup an exit handler to handle internal cleanup and possible | |
172 // callback when a process terminates. | |
173 int exitDataRead = 0; | |
174 final int EXIT_DATA_SIZE = 8; | |
175 List<int> exitDataBuffer = new List<int>(EXIT_DATA_SIZE); | |
176 _exitHandler.inputStream.onData = () { | |
177 | |
178 int exitCode(List<int> ints) { | |
179 var code = _intFromBytes(ints, 0); | |
180 var negative = _intFromBytes(ints, 4); | |
181 assert(negative == 0 || negative == 1); | |
182 return (negative == 0) ? code : -code; | |
183 } | |
184 | |
185 void handleExit() { | |
186 _ended = true; | |
187 if (_onExit !== null) { | |
188 _onExit(exitCode(exitDataBuffer)); | |
189 } | |
190 _out.close(); | |
191 } | |
192 | |
193 exitDataRead += _exitHandler.inputStream.readInto( | |
194 exitDataBuffer, exitDataRead, EXIT_DATA_SIZE - exitDataRead); | |
195 if (exitDataRead == EXIT_DATA_SIZE) handleExit(); | |
196 }; | |
197 | |
198 completer.complete(this); | |
199 }); | |
200 return completer.future; | |
201 } | |
202 | |
203 bool _startNative(String path, | |
204 List<String> arguments, | |
205 String workingDirectory, | |
206 List<String> environment, | |
207 Socket input, | |
208 Socket output, | |
209 Socket error, | |
210 Socket exitHandler, | |
211 _ProcessStartStatus status) native "Process_Start"; | |
212 | |
213 InputStream get stdout { | |
214 return _in.inputStream; | |
215 } | |
216 | |
217 InputStream get stderr { | |
218 return _err.inputStream; | |
219 } | |
220 | |
221 OutputStream get stdin { | |
222 return _out.outputStream; | |
223 } | |
224 | |
225 bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) { | |
226 if (signal is! ProcessSignal) { | |
227 throw new ArgumentError( | |
228 "Argument 'signal' must be a ProcessSignal"); | |
229 } | |
230 assert(_started); | |
231 if (_ended) return false; | |
232 return _kill(this, signal._signalNumber); | |
233 } | |
234 | |
235 bool _kill(Process p, int signal) native "Process_Kill"; | |
236 | |
237 void set onExit(void callback(int exitCode)) { | |
238 if (_ended) { | |
239 throw new ProcessException("Process killed"); | |
240 } | |
241 _onExit = callback; | |
242 } | |
243 | |
244 String _path; | |
245 List<String> _arguments; | |
246 String _workingDirectory; | |
247 List<String> _environment; | |
248 // Private methods of _Socket are used by _in, _out, and _err. | |
249 _Socket _in; | |
250 _Socket _out; | |
251 _Socket _err; | |
252 Socket _exitHandler; | |
253 bool _ended; | |
254 bool _started; | |
255 Function _onExit; | |
256 } | |
257 | |
258 | |
259 // _NonInteractiveProcess is a wrapper around an interactive process | |
260 // that buffers output so it can be delivered when the process exits. | |
261 // _NonInteractiveProcess is used to implement the Process.run | |
262 // method. | |
263 class _NonInteractiveProcess { | |
264 _NonInteractiveProcess(String path, | |
265 List<String> arguments, | |
266 ProcessOptions options) { | |
267 _completer = new Completer<ProcessResult>(); | |
268 // Extract output encoding options and verify arguments. | |
269 var stdoutEncoding = Encoding.UTF_8; | |
270 var stderrEncoding = Encoding.UTF_8; | |
271 if (options !== null) { | |
272 if (options.stdoutEncoding !== null) { | |
273 stdoutEncoding = options.stdoutEncoding; | |
274 if (stdoutEncoding is !Encoding) { | |
275 throw new ArgumentError( | |
276 'stdoutEncoding option is not an encoding: $stdoutEncoding'); | |
277 } | |
278 } | |
279 if (options.stderrEncoding !== null) { | |
280 stderrEncoding = options.stderrEncoding; | |
281 if (stderrEncoding is !Encoding) { | |
282 throw new ArgumentError( | |
283 'stderrEncoding option is not an encoding: $stderrEncoding'); | |
284 } | |
285 } | |
286 } | |
287 | |
288 // Start the underlying process. | |
289 var processFuture = new _Process(path, arguments, options)._start(); | |
290 | |
291 processFuture.then((Process p) { | |
292 // Make sure the process stdin is closed. | |
293 p.stdin.close; | |
294 | |
295 // Setup process exit handling. | |
296 p.onExit = (exitCode) { | |
297 _exitCode = exitCode; | |
298 _checkDone(); | |
299 }; | |
300 | |
301 // Setup stdout handling. | |
302 _stdoutBuffer = new StringBuffer(); | |
303 var stdoutStream = new StringInputStream(p.stdout, stdoutEncoding); | |
304 stdoutStream.onData = () { | |
305 var data = stdoutStream.read(); | |
306 if (data != null) _stdoutBuffer.add(data); | |
307 }; | |
308 stdoutStream.onClosed = () { | |
309 _stdoutClosed = true; | |
310 _checkDone(); | |
311 }; | |
312 | |
313 // Setup stderr handling. | |
314 _stderrBuffer = new StringBuffer(); | |
315 var stderrStream = new StringInputStream(p.stderr, stderrEncoding); | |
316 stderrStream.onData = () { | |
317 var data = stderrStream.read(); | |
318 if (data != null) _stderrBuffer.add(data); | |
319 }; | |
320 stderrStream.onClosed = () { | |
321 _stderrClosed = true; | |
322 _checkDone(); | |
323 }; | |
324 }); | |
325 | |
326 processFuture.handleException((error) { | |
327 _completer.completeException(error); | |
328 return true; | |
329 }); | |
330 } | |
331 | |
332 void _checkDone() { | |
333 if (_exitCode != null && _stderrClosed && _stdoutClosed) { | |
334 _completer.complete(new _ProcessResult(_exitCode, | |
335 _stdoutBuffer.toString(), | |
336 _stderrBuffer.toString())); | |
337 } | |
338 } | |
339 | |
340 Future<ProcessResult> get _result => _completer.future; | |
341 | |
342 Completer<ProcessResult> _completer; | |
343 StringBuffer _stdoutBuffer; | |
344 StringBuffer _stderrBuffer; | |
345 int _exitCode; | |
346 bool _stdoutClosed = false; | |
347 bool _stderrClosed = false; | |
348 } | |
349 | |
350 | |
351 class _ProcessResult implements ProcessResult { | |
352 const _ProcessResult(int this.exitCode, | |
353 String this.stdout, | |
354 String this.stderr); | |
355 | |
356 final int exitCode; | |
357 final String stdout; | |
358 final String stderr; | |
359 } | |
OLD | NEW |