spawn_and_wait_for_process: suppress js warning
[conkeror.git] / modules / spawn-process.js
blob2627d97e447e2b622c1319330e322fe6848ee6bd
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6 **/
8 in_module(null);
10 require("interactive.js");
11 require("io.js");
13 const WINDOWS = (get_os() == "WINNT");
14 const POSIX = !WINDOWS;
15 const PATH = getenv("PATH").split(POSIX ? ":" : ";");
17 const path_component_regexp = POSIX ? /^[^\/]+$/ : /^[^\/\\]+$/;
19 function get_file_in_path (name) {
20     if (name instanceof Ci.nsIFile) {
21         if (name.exists())
22             return name;
23         return null;
24     }
25     var file = Cc["@mozilla.org/file/local;1"]
26         .createInstance(Ci.nsILocalFile);
27     if (! path_component_regexp.test(name)) {
28         // Absolute path
29         try {
30             file.initWithPath(name);
31             if (file.exists())
32                 return file;
33         } catch (e) {}
34         return null;
35     } else {
36         // Relative path
37         for (var i = 0, plen = PATH.length; i < plen; ++i) {
38             try {
39                 file.initWithPath(PATH[i]);
40                 file.appendRelativePath(name);
41                 if (file.exists())
42                     return file;
43             } catch (e) {}
44         }
45     }
46     return null;
49 function spawn_process_internal (program, args, blocking) {
50     var process = Cc["@mozilla.org/process/util;1"]
51         .createInstance(Ci.nsIProcess);
52     process.init(get_file_in_path(program));
53     return process.run(!!blocking, args, args.length);
56 var PATH_programs = null;
57 function get_shell_command_completer () {
58     if (PATH_programs == null) {
59         PATH_programs = [];
60         var file = Cc["@mozilla.org/file/local;1"]
61             .createInstance(Ci.nsILocalFile);
62         for (var i = 0, plen = PATH.length; i < plen; ++i) {
63             try {
64                 file.initWithPath(PATH[i]);
65                 var entries = file.directoryEntries;
66                 while (entries.hasMoreElements()) {
67                     var entry = entries.getNext().QueryInterface(Ci.nsIFile);
68                     PATH_programs.push(entry.leafName);
69                 }
70             } catch (e) {}
71         }
72         PATH_programs.sort();
73     }
74     return prefix_completer($completions = PATH_programs,
75                             $get_string = function (x) x);
78 // use default
79 minibuffer_auto_complete_preferences["shell-command"] = null;
81 /* FIXME: support a relative or full path as well as PATH commands */
82 define_keywords("$cwd");
83 minibuffer.prototype.read_shell_command = function () {
84     keywords(arguments, $history = "shell-command");
85     var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd.path + "]:";
86     var result = yield this.read(
87         $prompt = prompt,
88         $history = "shell-command",
89         $auto_complete = "shell-command",
90         $select,
91         $validator = function (x, m) {
92             var s = x.replace(/^\s+|\s+$/g, '');
93             if (s.length == 0) {
94                 m.message("A blank shell command is not allowed.");
95                 return false;
96             }
97             return true;
98         },
99         forward_keywords(arguments),
100         $completer = get_shell_command_completer());
101     yield co_return(result);
104 const STDIN_FILENO = 0;
105 const STDOUT_FILENO = 1;
106 const STDERR_FILENO = 2;
108 var spawn_process_helper_default_fd_wait_timeout = 1000;
109 var spawn_process_helper_setup_timeout = 2000;
110 var spawn_process_helper_program = file_locator_service.get("CurProcD", Ci.nsIFile);
111 spawn_process_helper_program.append("conkeror-spawn-helper");
114  * @param program_name
115  *        Specifies the full path to the program.
116  * @param args
117  *        An array of strings to pass as the arguments to the program.  The
118  *        first argument should be the program name.  These strings must not
119  *        have any NUL bytes in them.
120  * @param working_dir       
121  *        If non-null, must be an nsILocalFile.  spawn_process will switch
122  *        to this path before running the program.
123  * @param finished_callback
124  *        Called with a single argument, the exit code of the process, as
125  *        returned by the wait system call.
126  * @param failure_callback
127  *        Called with a single argument, an exception, if one occurs.
128  * @param fds
129  *        If non-null, must be an object with only non-negative integer
130  *        properties set.  Each such property specifies that the corresponding
131  *        file descriptor in the spawned process should be redirected.  Note
132  *        that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
133  *        corresponds to STDERR.  Note that every redirected file descriptor can
134  *        be used for both input and output, although STDIN, STDOUT, and STDERR
135  *        are typically used only unidirectionally.  Each property must be an
136  *        object itself, with an input and/or output property specifying
137  *        callback functions that are called with an nsIAsyncInputStream or
138  *        nsIAsyncOutputStream when the stream for that file descriptor is
139  *        available.
140  * @param fd_wait_timeout
141  *        Specifies the number of milliseconds to wait for the file descriptor
142  *        redirection sockets to be closed after the control socket indicates
143  *        the process has exited before they are closed forcefully.  A negative
144  *        value means to wait indefinitely.  If fd_wait_timeout is null,
145  *        spawn_process_helper_default_fd_wait_timeout is used instead.
146  * @return
147  *        A function that can be called to prematurely terminate the spawned
148  *        process.
149  */
150 function spawn_process (program_name, args, working_dir,
151                         success_callback, failure_callback, fds,
152                         fd_wait_timeout) {
153     args = args.slice();
154     if (args[0] == null)
155         args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
157     program_name = get_file_in_path(program_name).path;
159     const key_length = 100;
160     const fd_spec_size = 15;
162     if (fds == null)
163         fds = {};
165     if (fd_wait_timeout === undefined)
166         fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
168     var unregistered_transports = [];
169     var registered_transports = [];
171     var server = null;
172     var setup_timer = null;
174     const CONTROL_CONNECTED = 0;
175     const CONTROL_SENDING_KEY = 1;
176     const CONTROL_SENT_KEY = 2;
178     var control_state = CONTROL_CONNECTED;
179     var terminate_pending = false;
181     var control_transport = null;
183     var control_binary_input_stream = null;
184     var control_output_stream = null, control_input_stream = null;
185     var exit_status = null;
187     var client_key = "";
188     var server_key = "";
189     // Make sure key does not have any 0 bytes in it.
190     for (let i = 0; i < key_length; ++i)
191         client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
193     // Make sure key does not have any 0 bytes in it.
194     for (let i = 0; i < key_length; ++i)
195         server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
197     var key_file_fd_data = "";
199     // This is the total number of redirected file descriptors.
200     var total_client_fds = 0;
202     // This is the total number of redirected file descriptors that will use a socket connection.
203     var total_fds = 0;
205     for (let i in fds) {
206         if (fds.hasOwnProperty(i)) {
207             if (fds[i] == null) {
208                 delete fds[i];
209                 continue;
210             }
211             key_file_fd_data += i + "\0";
212             let fd = fds[i];
213             if ('file' in fd) {
214                 if (fd.perms == null)
215                     fd.perms = 0666;
216                 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
217                 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
218             } else {
219                 ++total_fds;
220                 key_file_fd_data += "\0";
221             }
222             ++total_client_fds;
223         }
224     }
225     var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
226         (working_dir != null ? working_dir.path : "") + "\0" +
227         args.length + "\0" +
228         args.join("\0") + "\0" +
229         total_client_fds + "\0" + key_file_fd_data;
231     function fail (e) {
232         if (!terminate_pending) {
233             terminate();
234             if (failure_callback)
235                 failure_callback(e);
236         }
237     }
239     function cleanup_server () {
240         if (server) {
241             server.close();
242             server = null;
243         }
244         for (let i in unregistered_transports) {
245             unregistered_transports[i].close(0);
246             delete unregistered_transports[i];
247         }
248     }
250     function cleanup_fd_sockets () {
251         for (let i in registered_transports) {
252             registered_transports[i].transport.close(0);
253             delete registered_transports[i];
254         }
255     }
257     function cleanup_control () {
258         if (control_transport) {
259             control_binary_input_stream.close();
260             control_binary_input_stream = null;
261             control_transport.close(0);
262             control_transport = null;
263             control_input_stream = null;
264             control_output_stream = null;
265         }
266     }
268     function control_send_terminate () {
269         control_input_stream = null;
270         control_binary_input_stream.close();
271         control_binary_input_stream = null;
272         async_binary_write(control_output_stream, "\0", function () {
273             control_output_stream = null;
274             control_transport.close(0);
275             control_transport = null;
276         });
277     }
279     function terminate () {
280         if (terminate_pending)
281             return exit_status;
282         terminate_pending = true;
283         if (setup_timer) {
284             setup_timer.cancel();
285             setup_timer = null;
286         }
287         cleanup_server();
288         cleanup_fd_sockets();
289         if (control_transport) {
290             switch (control_state) {
291             case CONTROL_SENT_KEY:
292                 control_send_terminate();
293                 break;
294             case CONTROL_CONNECTED:
295                 cleanup_control();
296                 break;
297                 /**
298                  * case CONTROL_SENDING_KEY: in this case once the key
299                  * is sent, the terminate_pending flag will be noticed
300                  * and control_send_terminate will be called, so nothing
301                  * more needs to be done here.
302                  */
303             }
304         }
305         return exit_status;
306     }
308     function finished () {
309         // Only call success_callback if terminate was not already called
310         if (!terminate_pending) {
311             terminate();
312             if (success_callback)
313                 success_callback(exit_status);
314         }
315     }
317     // Create server socket to listen for connections from the external helper program
318     try {
319         server = Cc['@mozilla.org/network/server-socket;1']
320             .createInstance(Ci.nsIServerSocket);
322         var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
324         write_binary_file(key_file, key_file_data);
325         server.init(-1 /* choose a port automatically */,
326                     true /* bind to localhost only */,
327                     -1 /* select backlog size automatically */);
329         setup_timer = call_after_timeout(function () {
330             setup_timer = null;
331             if (control_state != CONTROL_SENT_KEY)
332                 fail("setup timeout");
333         }, spawn_process_helper_setup_timeout);
335         function wait_for_fd_sockets () {
336             var remaining_streams = total_fds * 2;
337             var timer = null;
338             function handler () {
339                 if (remaining_streams != null) {
340                     --remaining_streams;
341                     if (remaining_streams == 0) {
342                         if (timer)
343                             timer.cancel();
344                         finished();
345                     }
346                 }
347             }
348             for each (let f in registered_transports) {
349                 input_stream_async_wait(f.input, handler, false /* wait for closure */);
350                 output_stream_async_wait(f.output, handler, false /* wait for closure */);
351             }
352             if (fd_wait_timeout != null) {
353                 timer = call_after_timeout(function() {
354                     remaining_streams = null;
355                     finished();
356                 }, fd_wait_timeout);
357             }
358         }
360         var control_data = "";
362         function handle_control_input () {
363             if (terminate_pending)
364                 return;
365             try {
366                 let avail = control_input_stream.available();
367                 if (avail > 0) {
368                     control_data += control_binary_input_stream.readBytes(avail);
369                     var off = control_data.indexOf("\0");
370                     if (off >= 0) {
371                         let message = control_data.substring(0,off);
372                         exit_status = parseInt(message);
373                         cleanup_control();
374                         /* wait for all fd sockets to close? */
375                         if (total_fds > 0)
376                             wait_for_fd_sockets();
377                         else
378                             finished();
379                         return;
380                     }
381                 }
382                 input_stream_async_wait(control_input_stream, handle_control_input);
383             } catch (e) {
384                 // Control socket closed: terminate
385                 cleanup_control();
386                 fail(e);
387             }
388         }
390         var registered_fds = 0;
392         server.asyncListen(
393             {
394                 onSocketAccepted: function (server, transport) {
395                     unregistered_transports.push(transport);
396                     function remove_from_unregistered () {
397                         var i;
398                         i = unregistered_transports.indexOf(transport);
399                         if (i >= 0) {
400                             unregistered_transports.splice(i, 1);
401                             return true;
402                         }
403                         return false;
404                     }
405                     function close () {
406                         transport.close(0);
407                         remove_from_unregistered();
408                     }
409                     var received_data = "";
410                     var header_size = key_length + fd_spec_size;
412                     var in_stream, bin_stream, out_stream;
414                     function handle_input () {
415                         if (terminate_pending)
416                             return;
417                         try {
418                             let remaining = header_size - received_data.length;
419                             let avail = in_stream.available();
420                             if (avail > 0) {
421                                 if (avail > remaining)
422                                     avail = remaining;
423                                 received_data += bin_stream.readBytes(avail);
424                             }
425                             if (received_data.length < header_size) {
426                                 input_stream_async_wait(in_stream, handle_input);
427                                 return;
428                             } else {
429                                 if (received_data.substring(0, key_length) != client_key)
430                                     throw "Invalid key";
431                             }
432                         } catch (e) {
433                             close();
434                         }
435                         try {
436                             var fdspec = received_data.substring(key_length);
437                             if (fdspec.charCodeAt(0) == 0) {
439                                 // This is the control connection
440                                 if (control_transport)
441                                     throw "Control transport already exists";
442                                 control_transport = transport;
443                                 control_output_stream = out_stream;
444                                 control_input_stream = in_stream;
445                                 control_binary_input_stream = bin_stream;
446                                 remove_from_unregistered();
447                             } else {
448                                 var fd = parseInt(fdspec);
449                                 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
450                                     throw "Invalid fd";
451                                 remove_from_unregistered();
452                                 bin_stream = null;
453                                 registered_transports[fd] = {transport: transport,
454                                                              input: in_stream,
455                                                              output: out_stream};
456                                 ++registered_fds;
457                             }
458                             if (control_transport && registered_fds == total_fds) {
459                                 cleanup_server();
460                                 control_state = CONTROL_SENDING_KEY;
461                                 async_binary_write(control_output_stream, server_key,
462                                                    function (error) {
463                                                        if (error != null)
464                                                            fail(error);
465                                                        control_state = CONTROL_SENT_KEY;
466                                                        if (setup_timer) {
467                                                            setup_timer.cancel();
468                                                            setup_timer = null;
469                                                        }
470                                                        if (terminate_pending) {
471                                                            control_send_terminate();
472                                                        } else {
473                                                            for (let i in fds) {
474                                                                let f = fds[i];
475                                                                let t = registered_transports[i];
476                                                                if ('input' in f)
477                                                                    f.input(t.input);
478                                                                else
479                                                                    t.input.close();
480                                                                if ('output' in f)
481                                                                    f.output(t.output);
482                                                                else
483                                                                    t.output.close();
484                                                            }
485                                                        }
486                                                    });
487                                 input_stream_async_wait(control_input_stream, handle_control_input);
488                             }
489                         } catch (e) {
490                             fail(e);
491                         }
492                     }
494                     try {
495                         in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
496                         out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
497                         bin_stream = binary_input_stream(in_stream);
498                         input_stream_async_wait(in_stream, handle_input);
499                     } catch (e) {
500                         close();
501                     }
502                 },
503                 onStopListening: function (s, status) {
504                 }
505             });
507         spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
508         return terminate;
509     } catch (e) {
510         terminate();
512         if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
513             if (WINDOWS)
514                 throw new Error("Error spawning process: not yet supported on MS Windows");
515             else
516                 throw new Error("Error spawning process: conkeror-spawn-helper not found; try running \"make\"");
517         }
518         // Allow the exception to propagate to the caller
519         throw e;
520     }
524  * spawn_process_blind: spawn a process and forget about it
525  */
526 define_keywords("$cwd", "$fds");
527 function spawn_process_blind (program_name, args) {
528     keywords(arguments);
529     /* Check if we can use spawn_process_internal */
530     var cwd = arguments.$cwd;
531     var fds = arguments.$fds;
532     if (cwd == null && fds == null && args[0] == null)
533         spawn_process_internal(program_name, args.slice(1));
534     else {
535         spawn_process(program_name, args, cwd,
536                       null /* success callback */,
537                       null /* failure callback */,
538                       fds);
539     }
543 //  Keyword arguments: $cwd, $fds
544 function spawn_and_wait_for_process (program_name, args) {
545     keywords(arguments, $cwd = null, $fds = null);
546     var cc = yield CONTINUATION;
547     spawn_process(program_name, args, arguments.$cwd,
548                   cc, cc.throw,
549                   arguments.$fds);
550     var result = yield SUSPEND;
551     yield co_return(result);
554 // Keyword arguments: $cwd, $fds
555 function shell_command_blind (cmd) {
556     keywords(arguments);
557     /* Check if we can use spawn_process_internal */
558     var cwd = arguments.$cwd;
559     var fds = arguments.$fds;
561     var program_name;
562     var args;
564     if (POSIX) {
565         var full_cmd;
566         if (cwd)
567             full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
568         else
569             full_cmd = cmd;
570         program_name = getenv("SHELL") || "/bin/sh";
571         args = [null, "-c", full_cmd];
572     } else {
573         var full_cmd;
574         if (cwd) {
575             full_cmd = "";
576             if (cwd.path.match(/[a-z]:/i)) {
577                 full_cmd += cwd.path.substring(0,2) + " && ";
578             }
579             full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
580         } else
581             full_cmd = cmd;
583         /* Need to convert the single command-line into a list of
584             * arguments that will then get converted back into a *
585             command-line by Mozilla. */
586         var out = [null, "/C"];
587         var cur_arg = "";
588         var quoting = false;
589         for (var i = 0; i < full_cmd.length; ++i) {
590             var ch = full_cmd[i];
591             if (ch == " ") {
592                 if (quoting) {
593                     cur_arg += ch;
594                 } else {
595                     out.push(cur_arg);
596                     cur_arg = "";
597                 }
598                 continue;
599             }
600             if (ch == "\"") {
601                 quoting = !quoting;
602                 continue;
603             }
604             cur_arg += ch;
605         }
606         if (cur_arg.length > 0)
607             out.push(cur_arg);
608         program_name = "cmd.exe";
609         args = out;
610     }
611     spawn_process_blind(program_name, args, $fds = arguments.$fds);
614 function substitute_shell_command_argument (cmdline, argument) {
615     if (!cmdline.match("{}"))
616         return cmdline + " \"" + shell_quote(argument) + "\"";
617     else
618         return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
621 function shell_command_with_argument_blind (command, arg) {
622     shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
626  * Keyword arguments:
627  * $cwd: The current working directory for the process.
628  * $fds: File descriptors to use.
629  */
630 function shell_command (command) {
631     if (!POSIX)
632         throw new Error("shell_command: Your OS is not yet supported");
633     var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
634                                                   [null, "-c", command],
635                                                   forward_keywords(arguments));
636     yield co_return(result);
639 function shell_command_with_argument (command, arg) {
640     yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));
643 provide("spawn-process");