Upload to unstable as 1.0.3+git171024-2 with urgency=high
[conkeror.git] / modules / spawn-process.js
blob7bf5d7d9f6cc4d5db9c00dab56d2923bd54912c8
1 /**
2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2012 John J. Foerch
5 * Use, modification, and distribution are subject to the terms specified in the
6 * COPYING file.
7 **/
9 require("interactive.js");
10 require("io.js");
11 require("env.js");
13 function spawn_process_internal (program, args, blocking) {
14 var process = Cc["@mozilla.org/process/util;1"]
15 .createInstance(Ci.nsIProcess);
16 process.init(find_file_in_path(program));
17 return process.run(!!blocking, args, args.length);
20 var PATH_programs = null;
22 function shell_command_completer () {
23 if (PATH_programs == null) {
24 PATH_programs = [];
25 var file = Cc["@mozilla.org/file/local;1"]
26 .createInstance(Ci.nsILocalFile);
27 for (var i = 0, plen = PATH.length; i < plen; ++i) {
28 try {
29 file.initWithPath(PATH[i]);
30 var entries = file.directoryEntries;
31 while (entries.hasMoreElements()) {
32 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
33 PATH_programs.push(entry.leafName);
35 } catch (e) {}
37 PATH_programs.sort();
39 prefix_completer.call(this, $completions = PATH_programs);
41 shell_command_completer.prototype = {
42 constructor: shell_command_completer,
43 __proto__: prefix_completer.prototype,
44 toString: function () "#<shell_command_completer>"
47 // use default
48 minibuffer_auto_complete_preferences["shell-command"] = null;
50 /* FIXME: support a relative or full path as well as PATH commands */
51 define_keywords("$cwd");
52 minibuffer.prototype.read_shell_command = function () {
53 keywords(arguments, $history = "shell-command");
54 var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd.path + "]:";
55 var result = yield this.read(
56 $prompt = prompt,
57 $history = "shell-command",
58 $auto_complete = "shell-command",
59 $select,
60 $validator = function (x, m) {
61 var s = x.replace(/^\s+|\s+$/g, '');
62 if (s.length == 0) {
63 m.message("A blank shell command is not allowed.");
64 return false;
66 return true;
68 forward_keywords(arguments),
69 $completer = new shell_command_completer());
70 yield co_return(result);
73 function find_spawn_helper () {
74 var f = file_locator_service.get("CurProcD", Ci.nsIFile);
75 f.append("conkeror-spawn-helper");
76 if (f.exists())
77 return f;
78 return find_file_in_path("conkeror-spawn-helper");
81 const STDIN_FILENO = 0;
82 const STDOUT_FILENO = 1;
83 const STDERR_FILENO = 2;
85 var spawn_process_helper_default_fd_wait_timeout = 1000;
86 var spawn_process_helper_setup_timeout = 2000;
88 /**
89 * @param program_name
90 * Specifies the full path to the program.
91 * @param args
92 * An array of strings to pass as the arguments to the program. The
93 * first argument should be the program name. These strings must not
94 * have any NUL bytes in them.
95 * @param working_dir
96 * If non-null, must be an nsILocalFile. spawn_process will switch
97 * to this path before running the program.
98 * @param fds
99 * If non-null, must be an object with only non-negative integer
100 * properties set. Each such property specifies that the corresponding
101 * file descriptor in the spawned process should be redirected. Note
102 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
103 * corresponds to STDERR. Note that every redirected file descriptor can
104 * be used for both input and output, although STDIN, STDOUT, and STDERR
105 * are typically used only unidirectionally. Each property must be an
106 * object itself, with an input and/or output property specifying
107 * callback functions that are called with an nsIAsyncInputStream or
108 * nsIAsyncOutputStream when the stream for that file descriptor is
109 * available.
110 * @param fd_wait_timeout
111 * Specifies the number of milliseconds to wait for the file descriptor
112 * redirection sockets to be closed after the control socket indicates
113 * the process has exited before they are closed forcefully. A negative
114 * value means to wait indefinitely. If fd_wait_timeout is null,
115 * spawn_process_helper_default_fd_wait_timeout is used instead.
116 * @return
117 * A function that can be called to prematurely terminate the spawned
118 * process.
120 function spawn_process (program_name, args, working_dir,
121 fds, fd_wait_timeout) {
123 let deferred = Promise.defer();
125 var spawn_process_helper_program = find_spawn_helper();
126 if (spawn_process_helper_program == null)
127 throw new Error("Error spawning process: conkeror-spawn-helper not found");
128 args = args.slice();
129 if (args[0] == null)
130 args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
132 program_name = find_file_in_path(program_name).path;
134 const key_length = 100;
135 const fd_spec_size = 15;
137 if (fds == null)
138 fds = {};
140 if (fd_wait_timeout === undefined)
141 fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
143 var unregistered_transports = [];
144 var registered_transports = [];
146 var server = null;
147 var setup_timer = null;
149 const CONTROL_CONNECTED = 0;
150 const CONTROL_SENDING_KEY = 1;
151 const CONTROL_SENT_KEY = 2;
153 var control_state = CONTROL_CONNECTED;
154 var terminate_pending = false;
156 var control_transport = null;
158 var control_binary_input_stream = null;
159 var control_output_stream = null, control_input_stream = null;
160 var exit_status = null;
162 var client_key = "";
163 var server_key = "";
164 // Make sure key does not have any 0 bytes in it.
165 for (let i = 0; i < key_length; ++i)
166 client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
168 // Make sure key does not have any 0 bytes in it.
169 for (let i = 0; i < key_length; ++i)
170 server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
172 var key_file_fd_data = "";
174 // This is the total number of redirected file descriptors.
175 var total_client_fds = 0;
177 // This is the total number of redirected file descriptors that will use a socket connection.
178 var total_fds = 0;
180 for (let i in fds) {
181 if (fds.hasOwnProperty(i)) {
182 if (fds[i] == null) {
183 delete fds[i];
184 continue;
186 key_file_fd_data += i + "\0";
187 let fd = fds[i];
188 if ('file' in fd) {
189 if (fd.perms == null)
190 fd.perms = parseInt("0666", 8);
191 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
192 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
193 } else {
194 ++total_fds;
195 key_file_fd_data += "\0";
197 ++total_client_fds;
200 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
201 (working_dir != null ? working_dir.path : "") + "\0" +
202 args.length + "\0" +
203 args.join("\0") + "\0" +
204 total_client_fds + "\0" + key_file_fd_data;
206 function fail (e) {
207 if (!terminate_pending) {
208 deferred.reject(e);
209 terminate();
213 function cleanup_server () {
214 if (server) {
215 server.close();
216 server = null;
218 for (let i in unregistered_transports) {
219 unregistered_transports[i].close(0);
220 delete unregistered_transports[i];
224 function cleanup_fd_sockets () {
225 for (let i in registered_transports) {
226 registered_transports[i].transport.close(0);
227 delete registered_transports[i];
231 function cleanup_control () {
232 if (control_transport) {
233 control_binary_input_stream.close();
234 control_binary_input_stream = null;
235 control_transport.close(0);
236 control_transport = null;
237 control_input_stream = null;
238 control_output_stream = null;
242 function control_send_terminate () {
243 control_input_stream = null;
244 control_binary_input_stream.close();
245 control_binary_input_stream = null;
246 async_binary_write(control_output_stream, "\0", function () {
247 control_output_stream = null;
248 control_transport.close(0);
249 control_transport = null;
253 function terminate () {
254 if (terminate_pending)
255 return exit_status;
256 terminate_pending = true;
257 if (setup_timer) {
258 setup_timer.cancel();
259 setup_timer = null;
261 cleanup_server();
262 cleanup_fd_sockets();
263 if (control_transport) {
264 switch (control_state) {
265 case CONTROL_SENT_KEY:
266 control_send_terminate();
267 break;
268 case CONTROL_CONNECTED:
269 cleanup_control();
270 break;
272 * case CONTROL_SENDING_KEY: in this case once the key
273 * is sent, the terminate_pending flag will be noticed
274 * and control_send_terminate will be called, so nothing
275 * more needs to be done here.
279 return exit_status;
282 function canceler (e) {
283 if (!terminate_pending) {
284 deferred.reject(e);
285 terminate();
289 function finished () {
290 if (!terminate_pending) {
291 deferred.resolve(exit_status);
292 terminate();
296 // Create server socket to listen for connections from the external helper program
297 try {
298 server = Cc['@mozilla.org/network/server-socket;1']
299 .createInstance(Ci.nsIServerSocket);
301 var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
303 write_binary_file(key_file, key_file_data);
304 server.init(-1 /* choose a port automatically */,
305 true /* bind to localhost only */,
306 -1 /* select backlog size automatically */);
308 setup_timer = call_after_timeout(function () {
309 setup_timer = null;
310 if (control_state != CONTROL_SENT_KEY)
311 fail("setup timeout");
312 }, spawn_process_helper_setup_timeout);
314 var wait_for_fd_sockets = function wait_for_fd_sockets () {
315 var remaining_streams = total_fds * 2;
316 var timer = null;
317 function handler () {
318 if (remaining_streams != null) {
319 --remaining_streams;
320 if (remaining_streams == 0) {
321 if (timer)
322 timer.cancel();
323 finished();
327 for each (let f in registered_transports) {
328 input_stream_async_wait(f.input, handler, false /* wait for closure */);
329 output_stream_async_wait(f.output, handler, false /* wait for closure */);
331 if (fd_wait_timeout != null) {
332 timer = call_after_timeout(function() {
333 remaining_streams = null;
334 finished();
335 }, fd_wait_timeout);
339 var control_data = "";
341 var handle_control_input = function handle_control_input () {
342 if (terminate_pending)
343 return;
344 try {
345 let avail = control_input_stream.available();
346 if (avail > 0) {
347 control_data += control_binary_input_stream.readBytes(avail);
348 var off = control_data.indexOf("\0");
349 if (off >= 0) {
350 let message = control_data.substring(0,off);
351 exit_status = parseInt(message);
352 cleanup_control();
353 /* wait for all fd sockets to close? */
354 if (total_fds > 0)
355 wait_for_fd_sockets();
356 else
357 finished();
358 return;
361 input_stream_async_wait(control_input_stream, handle_control_input);
362 } catch (e) {
363 // Control socket closed: terminate
364 cleanup_control();
365 fail(e);
369 var registered_fds = 0;
371 server.asyncListen(
373 onSocketAccepted: function (server, transport) {
374 unregistered_transports.push(transport);
375 function remove_from_unregistered () {
376 var i;
377 i = unregistered_transports.indexOf(transport);
378 if (i >= 0) {
379 unregistered_transports.splice(i, 1);
380 return true;
382 return false;
384 function close () {
385 transport.close(0);
386 remove_from_unregistered();
388 var received_data = "";
389 var header_size = key_length + fd_spec_size;
391 var in_stream, bin_stream, out_stream;
393 function handle_input () {
394 if (terminate_pending)
395 return;
396 try {
397 let remaining = header_size - received_data.length;
398 let avail = in_stream.available();
399 if (avail > 0) {
400 if (avail > remaining)
401 avail = remaining;
402 received_data += bin_stream.readBytes(avail);
404 if (received_data.length < header_size) {
405 input_stream_async_wait(in_stream, handle_input);
406 return;
407 } else {
408 if (received_data.substring(0, key_length) != client_key)
409 throw "Invalid key";
411 } catch (e) {
412 close();
414 try {
415 var fdspec = received_data.substring(key_length);
416 if (fdspec.charCodeAt(0) == 0) {
418 // This is the control connection
419 if (control_transport)
420 throw "Control transport already exists";
421 control_transport = transport;
422 control_output_stream = out_stream;
423 control_input_stream = in_stream;
424 control_binary_input_stream = bin_stream;
425 remove_from_unregistered();
426 } else {
427 var fd = parseInt(fdspec);
428 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
429 throw "Invalid fd";
430 remove_from_unregistered();
431 bin_stream = null;
432 registered_transports[fd] = {transport: transport,
433 input: in_stream,
434 output: out_stream};
435 ++registered_fds;
437 if (control_transport && registered_fds == total_fds) {
438 cleanup_server();
439 control_state = CONTROL_SENDING_KEY;
440 async_binary_write(control_output_stream, server_key,
441 function (error) {
442 if (error != null)
443 fail(error);
444 control_state = CONTROL_SENT_KEY;
445 if (setup_timer) {
446 setup_timer.cancel();
447 setup_timer = null;
449 if (terminate_pending) {
450 control_send_terminate();
451 } else {
452 for (let i in fds) {
453 let f = fds[i];
454 let t = registered_transports[i];
455 if ('input' in f)
456 f.input(t.input);
457 else
458 t.input.close();
459 if ('output' in f)
460 f.output(t.output);
461 else
462 t.output.close();
466 input_stream_async_wait(control_input_stream, handle_control_input);
468 } catch (e) {
469 fail(e);
473 try {
474 in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
475 out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
476 bin_stream = binary_input_stream(in_stream);
477 input_stream_async_wait(in_stream, handle_input);
478 } catch (e) {
479 close();
482 onStopListening: function (s, status) {
486 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
487 return make_cancelable(deferred.promise, canceler);
488 } catch (e) {
489 terminate();
491 if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
492 if (WINDOWS)
493 throw new Error("Error spawning process: not yet supported on MS Windows");
494 else
495 throw new Error("Error spawning process: conkeror-spawn-helper not found");
497 // Allow the exception to propagate to the caller
498 throw e;
503 * spawn_process_blind: spawn a process and forget about it
505 define_keywords("$cwd", "$fds");
506 function spawn_process_blind (program_name, args) {
507 keywords(arguments);
508 /* Check if we can use spawn_process_internal */
509 var cwd = arguments.$cwd;
510 var fds = arguments.$fds;
511 if (cwd == null && fds == null && args[0] == null)
512 spawn_process_internal(program_name, args.slice(1));
513 else {
514 spawn_process(program_name, args, cwd, fds);
519 // Keyword arguments: $cwd, $fds
520 function spawn_and_wait_for_process (program_name, args) {
521 keywords(arguments, $cwd = null, $fds = null);
522 let result = yield spawn_process(program_name, args, arguments.$cwd,
523 arguments.$fds);
524 yield co_return(result);
527 // Keyword arguments: $cwd, $fds
528 function shell_command_blind (cmd) {
529 keywords(arguments);
530 /* Check if we can use spawn_process_internal */
531 var cwd = arguments.$cwd;
532 var fds = arguments.$fds;
534 var program_name;
535 var args;
537 if (POSIX) {
538 var full_cmd;
539 if (cwd)
540 full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
541 else
542 full_cmd = cmd;
543 program_name = getenv("SHELL") || "/bin/sh";
544 args = [null, "-c", full_cmd];
545 } else {
546 var full_cmd;
547 if (cwd) {
548 full_cmd = "";
549 if (cwd.path.match(/[a-z]:/i)) {
550 full_cmd += cwd.path.substring(0,2) + " && ";
552 full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
553 } else
554 full_cmd = cmd;
556 /* Need to convert the single command-line into a list of
557 * arguments that will then get converted back into a *
558 command-line by Mozilla. */
559 var out = [null, "/C"];
560 var cur_arg = "";
561 var quoting = false;
562 for (var i = 0; i < full_cmd.length; ++i) {
563 var ch = full_cmd[i];
564 if (ch == " ") {
565 if (quoting) {
566 cur_arg += ch;
567 } else {
568 out.push(cur_arg);
569 cur_arg = "";
571 continue;
573 if (ch == "\"") {
574 quoting = !quoting;
575 continue;
577 cur_arg += ch;
579 if (cur_arg.length > 0)
580 out.push(cur_arg);
581 program_name = "cmd.exe";
582 args = out;
584 spawn_process_blind(program_name, args, $fds = arguments.$fds);
587 function substitute_shell_command_argument (cmdline, argument) {
588 if (!cmdline.match("{}"))
589 return cmdline + " \"" + shell_quote(argument) + "\"";
590 else
591 return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
594 function shell_command_with_argument_blind (command, arg) {
595 shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
599 * Keyword arguments:
600 * $cwd: The current working directory for the process.
601 * $fds: File descriptors to use.
603 function shell_command (command) {
604 if (!POSIX)
605 throw new Error("shell_command: Your OS is not yet supported");
606 var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
607 [null, "-c", command],
608 forward_keywords(arguments));
609 yield co_return(result);
612 function shell_command_with_argument (command, arg) {
613 yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));
616 provide("spawn-process");