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