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