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