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