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