various whitespace
[conkeror.git] / modules / spawn-process.js
blob83427f58ee6a0a1053e1ef498f0438ed428401f8
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2012 John J. Foerch
4  *
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);
34                 }
35             } catch (e) {}
36         }
37         PATH_programs.sort();
38     }
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;
65             }
66             return true;
67         },
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 finished_callback
99  *        Called with a single argument, the exit code of the process, as
100  *        returned by the wait system call.
101  * @param failure_callback
102  *        Called with a single argument, an exception, if one occurs.
103  * @param fds
104  *        If non-null, must be an object with only non-negative integer
105  *        properties set.  Each such property specifies that the corresponding
106  *        file descriptor in the spawned process should be redirected.  Note
107  *        that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
108  *        corresponds to STDERR.  Note that every redirected file descriptor can
109  *        be used for both input and output, although STDIN, STDOUT, and STDERR
110  *        are typically used only unidirectionally.  Each property must be an
111  *        object itself, with an input and/or output property specifying
112  *        callback functions that are called with an nsIAsyncInputStream or
113  *        nsIAsyncOutputStream when the stream for that file descriptor is
114  *        available.
115  * @param fd_wait_timeout
116  *        Specifies the number of milliseconds to wait for the file descriptor
117  *        redirection sockets to be closed after the control socket indicates
118  *        the process has exited before they are closed forcefully.  A negative
119  *        value means to wait indefinitely.  If fd_wait_timeout is null,
120  *        spawn_process_helper_default_fd_wait_timeout is used instead.
121  * @return
122  *        A function that can be called to prematurely terminate the spawned
123  *        process.
124  */
125 function spawn_process (program_name, args, working_dir,
126                         fds, fd_wait_timeout) {
128     let deferred = Promise.defer();
130     var spawn_process_helper_program = find_spawn_helper();
131     if (spawn_process_helper_program == null)
132         throw new Error("Error spawning process: conkeror-spawn-helper not found");
133     args = args.slice();
134     if (args[0] == null)
135         args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
137     program_name = find_file_in_path(program_name).path;
139     const key_length = 100;
140     const fd_spec_size = 15;
142     if (fds == null)
143         fds = {};
145     if (fd_wait_timeout === undefined)
146         fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
148     var unregistered_transports = [];
149     var registered_transports = [];
151     var server = null;
152     var setup_timer = null;
154     const CONTROL_CONNECTED = 0;
155     const CONTROL_SENDING_KEY = 1;
156     const CONTROL_SENT_KEY = 2;
158     var control_state = CONTROL_CONNECTED;
159     var terminate_pending = false;
161     var control_transport = null;
163     var control_binary_input_stream = null;
164     var control_output_stream = null, control_input_stream = null;
165     var exit_status = null;
167     var client_key = "";
168     var server_key = "";
169     // Make sure key does not have any 0 bytes in it.
170     for (let i = 0; i < key_length; ++i)
171         client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
173     // Make sure key does not have any 0 bytes in it.
174     for (let i = 0; i < key_length; ++i)
175         server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
177     var key_file_fd_data = "";
179     // This is the total number of redirected file descriptors.
180     var total_client_fds = 0;
182     // This is the total number of redirected file descriptors that will use a socket connection.
183     var total_fds = 0;
185     for (let i in fds) {
186         if (fds.hasOwnProperty(i)) {
187             if (fds[i] == null) {
188                 delete fds[i];
189                 continue;
190             }
191             key_file_fd_data += i + "\0";
192             let fd = fds[i];
193             if ('file' in fd) {
194                 if (fd.perms == null)
195                     fd.perms = 0666;
196                 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
197                 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
198             } else {
199                 ++total_fds;
200                 key_file_fd_data += "\0";
201             }
202             ++total_client_fds;
203         }
204     }
205     var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
206         (working_dir != null ? working_dir.path : "") + "\0" +
207         args.length + "\0" +
208         args.join("\0") + "\0" +
209         total_client_fds + "\0" + key_file_fd_data;
211     function fail (e) {
212         if (!terminate_pending) {
213             deferred.reject(e);
214             terminate();
215         }
216     }
218     function cleanup_server () {
219         if (server) {
220             server.close();
221             server = null;
222         }
223         for (let i in unregistered_transports) {
224             unregistered_transports[i].close(0);
225             delete unregistered_transports[i];
226         }
227     }
229     function cleanup_fd_sockets () {
230         for (let i in registered_transports) {
231             registered_transports[i].transport.close(0);
232             delete registered_transports[i];
233         }
234     }
236     function cleanup_control () {
237         if (control_transport) {
238             control_binary_input_stream.close();
239             control_binary_input_stream = null;
240             control_transport.close(0);
241             control_transport = null;
242             control_input_stream = null;
243             control_output_stream = null;
244         }
245     }
247     function control_send_terminate () {
248         control_input_stream = null;
249         control_binary_input_stream.close();
250         control_binary_input_stream = null;
251         async_binary_write(control_output_stream, "\0", function () {
252             control_output_stream = null;
253             control_transport.close(0);
254             control_transport = null;
255         });
256     }
258     function terminate () {
259         if (terminate_pending)
260             return exit_status;
261         terminate_pending = true;
262         if (setup_timer) {
263             setup_timer.cancel();
264             setup_timer = null;
265         }
266         cleanup_server();
267         cleanup_fd_sockets();
268         if (control_transport) {
269             switch (control_state) {
270             case CONTROL_SENT_KEY:
271                 control_send_terminate();
272                 break;
273             case CONTROL_CONNECTED:
274                 cleanup_control();
275                 break;
276                 /**
277                  * case CONTROL_SENDING_KEY: in this case once the key
278                  * is sent, the terminate_pending flag will be noticed
279                  * and control_send_terminate will be called, so nothing
280                  * more needs to be done here.
281                  */
282             }
283         }
284         return exit_status;
285     }
287     function canceler (e) {
288         if (!terminate_pending) {
289             deferred.reject(e);
290             terminate();
291         }
292     }
294     function finished () {
295         // Only call success_callback if terminate was not already called
296         if (!terminate_pending) {
297             deferred.resolve(exit_status);
298             terminate();
299         }
300     }
302     // Create server socket to listen for connections from the external helper program
303     try {
304         server = Cc['@mozilla.org/network/server-socket;1']
305             .createInstance(Ci.nsIServerSocket);
307         var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
309         write_binary_file(key_file, key_file_data);
310         server.init(-1 /* choose a port automatically */,
311                     true /* bind to localhost only */,
312                     -1 /* select backlog size automatically */);
314         setup_timer = call_after_timeout(function () {
315             setup_timer = null;
316             if (control_state != CONTROL_SENT_KEY)
317                 fail("setup timeout");
318         }, spawn_process_helper_setup_timeout);
320         function wait_for_fd_sockets () {
321             var remaining_streams = total_fds * 2;
322             var timer = null;
323             function handler () {
324                 if (remaining_streams != null) {
325                     --remaining_streams;
326                     if (remaining_streams == 0) {
327                         if (timer)
328                             timer.cancel();
329                         finished();
330                     }
331                 }
332             }
333             for each (let f in registered_transports) {
334                 input_stream_async_wait(f.input, handler, false /* wait for closure */);
335                 output_stream_async_wait(f.output, handler, false /* wait for closure */);
336             }
337             if (fd_wait_timeout != null) {
338                 timer = call_after_timeout(function() {
339                     remaining_streams = null;
340                     finished();
341                 }, fd_wait_timeout);
342             }
343         }
345         var control_data = "";
347         function handle_control_input () {
348             if (terminate_pending)
349                 return;
350             try {
351                 let avail = control_input_stream.available();
352                 if (avail > 0) {
353                     control_data += control_binary_input_stream.readBytes(avail);
354                     var off = control_data.indexOf("\0");
355                     if (off >= 0) {
356                         let message = control_data.substring(0,off);
357                         exit_status = parseInt(message);
358                         cleanup_control();
359                         /* wait for all fd sockets to close? */
360                         if (total_fds > 0)
361                             wait_for_fd_sockets();
362                         else
363                             finished();
364                         return;
365                     }
366                 }
367                 input_stream_async_wait(control_input_stream, handle_control_input);
368             } catch (e) {
369                 // Control socket closed: terminate
370                 cleanup_control();
371                 fail(e);
372             }
373         }
375         var registered_fds = 0;
377         server.asyncListen(
378             {
379                 onSocketAccepted: function (server, transport) {
380                     unregistered_transports.push(transport);
381                     function remove_from_unregistered () {
382                         var i;
383                         i = unregistered_transports.indexOf(transport);
384                         if (i >= 0) {
385                             unregistered_transports.splice(i, 1);
386                             return true;
387                         }
388                         return false;
389                     }
390                     function close () {
391                         transport.close(0);
392                         remove_from_unregistered();
393                     }
394                     var received_data = "";
395                     var header_size = key_length + fd_spec_size;
397                     var in_stream, bin_stream, out_stream;
399                     function handle_input () {
400                         if (terminate_pending)
401                             return;
402                         try {
403                             let remaining = header_size - received_data.length;
404                             let avail = in_stream.available();
405                             if (avail > 0) {
406                                 if (avail > remaining)
407                                     avail = remaining;
408                                 received_data += bin_stream.readBytes(avail);
409                             }
410                             if (received_data.length < header_size) {
411                                 input_stream_async_wait(in_stream, handle_input);
412                                 return;
413                             } else {
414                                 if (received_data.substring(0, key_length) != client_key)
415                                     throw "Invalid key";
416                             }
417                         } catch (e) {
418                             close();
419                         }
420                         try {
421                             var fdspec = received_data.substring(key_length);
422                             if (fdspec.charCodeAt(0) == 0) {
424                                 // This is the control connection
425                                 if (control_transport)
426                                     throw "Control transport already exists";
427                                 control_transport = transport;
428                                 control_output_stream = out_stream;
429                                 control_input_stream = in_stream;
430                                 control_binary_input_stream = bin_stream;
431                                 remove_from_unregistered();
432                             } else {
433                                 var fd = parseInt(fdspec);
434                                 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
435                                     throw "Invalid fd";
436                                 remove_from_unregistered();
437                                 bin_stream = null;
438                                 registered_transports[fd] = {transport: transport,
439                                                              input: in_stream,
440                                                              output: out_stream};
441                                 ++registered_fds;
442                             }
443                             if (control_transport && registered_fds == total_fds) {
444                                 cleanup_server();
445                                 control_state = CONTROL_SENDING_KEY;
446                                 async_binary_write(control_output_stream, server_key,
447                                                    function (error) {
448                                                        if (error != null)
449                                                            fail(error);
450                                                        control_state = CONTROL_SENT_KEY;
451                                                        if (setup_timer) {
452                                                            setup_timer.cancel();
453                                                            setup_timer = null;
454                                                        }
455                                                        if (terminate_pending) {
456                                                            control_send_terminate();
457                                                        } else {
458                                                            for (let i in fds) {
459                                                                let f = fds[i];
460                                                                let t = registered_transports[i];
461                                                                if ('input' in f)
462                                                                    f.input(t.input);
463                                                                else
464                                                                    t.input.close();
465                                                                if ('output' in f)
466                                                                    f.output(t.output);
467                                                                else
468                                                                    t.output.close();
469                                                            }
470                                                        }
471                                                    });
472                                 input_stream_async_wait(control_input_stream, handle_control_input);
473                             }
474                         } catch (e) {
475                             fail(e);
476                         }
477                     }
479                     try {
480                         in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
481                         out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
482                         bin_stream = binary_input_stream(in_stream);
483                         input_stream_async_wait(in_stream, handle_input);
484                     } catch (e) {
485                         close();
486                     }
487                 },
488                 onStopListening: function (s, status) {
489                 }
490             });
492         spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
493         return make_cancelable(deferred.promise, canceler);
494     } catch (e) {
495         terminate();
497         if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
498             if (WINDOWS)
499                 throw new Error("Error spawning process: not yet supported on MS Windows");
500             else
501                 throw new Error("Error spawning process: conkeror-spawn-helper not found");
502         }
503         // Allow the exception to propagate to the caller
504         throw e;
505     }
509  * spawn_process_blind: spawn a process and forget about it
510  */
511 define_keywords("$cwd", "$fds");
512 function spawn_process_blind (program_name, args) {
513     keywords(arguments);
514     /* Check if we can use spawn_process_internal */
515     var cwd = arguments.$cwd;
516     var fds = arguments.$fds;
517     if (cwd == null && fds == null && args[0] == null)
518         spawn_process_internal(program_name, args.slice(1));
519     else {
520         spawn_process(program_name, args, cwd, fds);
521     }
525 //  Keyword arguments: $cwd, $fds
526 function spawn_and_wait_for_process (program_name, args) {
527     keywords(arguments, $cwd = null, $fds = null);
528     let result = yield spawn_process(program_name, args, arguments.$cwd,
529                                      arguments.$fds);
530     yield co_return(result);
533 // Keyword arguments: $cwd, $fds
534 function shell_command_blind (cmd) {
535     keywords(arguments);
536     /* Check if we can use spawn_process_internal */
537     var cwd = arguments.$cwd;
538     var fds = arguments.$fds;
540     var program_name;
541     var args;
543     if (POSIX) {
544         var full_cmd;
545         if (cwd)
546             full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
547         else
548             full_cmd = cmd;
549         program_name = getenv("SHELL") || "/bin/sh";
550         args = [null, "-c", full_cmd];
551     } else {
552         var full_cmd;
553         if (cwd) {
554             full_cmd = "";
555             if (cwd.path.match(/[a-z]:/i)) {
556                 full_cmd += cwd.path.substring(0,2) + " && ";
557             }
558             full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
559         } else
560             full_cmd = cmd;
562         /* Need to convert the single command-line into a list of
563             * arguments that will then get converted back into a *
564             command-line by Mozilla. */
565         var out = [null, "/C"];
566         var cur_arg = "";
567         var quoting = false;
568         for (var i = 0; i < full_cmd.length; ++i) {
569             var ch = full_cmd[i];
570             if (ch == " ") {
571                 if (quoting) {
572                     cur_arg += ch;
573                 } else {
574                     out.push(cur_arg);
575                     cur_arg = "";
576                 }
577                 continue;
578             }
579             if (ch == "\"") {
580                 quoting = !quoting;
581                 continue;
582             }
583             cur_arg += ch;
584         }
585         if (cur_arg.length > 0)
586             out.push(cur_arg);
587         program_name = "cmd.exe";
588         args = out;
589     }
590     spawn_process_blind(program_name, args, $fds = arguments.$fds);
593 function substitute_shell_command_argument (cmdline, argument) {
594     if (!cmdline.match("{}"))
595         return cmdline + " \"" + shell_quote(argument) + "\"";
596     else
597         return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
600 function shell_command_with_argument_blind (command, arg) {
601     shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
605  * Keyword arguments:
606  * $cwd: The current working directory for the process.
607  * $fds: File descriptors to use.
608  */
609 function shell_command (command) {
610     if (!POSIX)
611         throw new Error("shell_command: Your OS is not yet supported");
612     var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
613                                                   [null, "-c", command],
614                                                   forward_keywords(arguments));
615     yield co_return(result);
618 function shell_command_with_argument (command, arg) {
619     yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));
622 provide("spawn-process");