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