1 require("interactive.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) {
16 var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
17 if (!path_component_regexp.test(name)) {
20 file.initWithPath(name);
27 for (var i = 0; i < PATH.length; ++i) {
29 file.initWithPath(PATH[i]);
30 file.appendRelativePath(name);
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) {
49 var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
50 for (var i = 0; i < PATH.length; ++i) {
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);
63 return prefix_completer($completions = PATH_programs,
64 $get_string = function (x) { return x; });
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(
77 $history = "shell-command",
78 $auto_complete = "shell-command",
80 $validator = function (x, m) {
81 var s = x.replace(/^\s+|\s+$/g, '');
83 m.message("A blank shell command is not allowed.");
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.
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.
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.
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
131 * A function that can be called to prematurely terminate the spawned process.
133 function spawn_process(program_name, args, working_dir,
134 success_callback, failure_callback, fds,
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;
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 = [];
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;
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.
188 if (fds.hasOwnProperty(i)) {
189 key_file_fd_data += i + "\0";
192 if (fd.perms == null)
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
201 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
202 (working_dir != null ? working_dir : "") + "\0" +
204 args.join("\0") + "\0" +
205 total_client_fds + "\0" + key_file_fd_data;
208 if (!terminate_pending) {
210 if (failure_callback)
215 function cleanup_server() {
220 for (let i in unregistered_transports) {
221 unregistered_transports[i].close(0);
222 delete unregistered_transports[i];
226 function cleanup_fd_sockets() {
227 for (let i in registered_transports) {
228 registered_transports[i].transport.close(0);
229 delete registered_transports[i];
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;
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;
255 function terminate() {
256 if (terminate_pending)
258 terminate_pending = true;
260 setup_timer.cancel();
264 cleanup_fd_sockets();
265 if (control_transport) {
266 switch (control_state) {
267 case CONTROL_SENT_KEY:
268 control_send_terminate();
270 case CONTROL_CONNECTED:
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.
284 function finished() {
285 // Only call success_callback if terminate was not already called
286 if (!terminate_pending) {
288 if (success_callback)
289 success_callback(exit_status);
293 // Create server socket to listen for connections from the external helper program
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 () {
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;
315 if (remaining_streams != null) {
317 if (remaining_streams == 0) {
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 */);
328 if (fd_wait_timeout != null) {
329 timer = call_after_timeout(function() {
330 remaining_streams = null;
336 var control_data = "";
338 function handle_control_input() {
339 if (terminate_pending)
342 let avail = control_input_stream.available();
344 control_data += control_binary_input_stream.readBytes(avail);
345 var off = control_data.indexOf("\0");
347 let message = control_data.substring(0,off);
348 exit_status = parseInt(message);
350 /* wait for all fd sockets to close? */
352 wait_for_fd_sockets();
358 input_stream_async_wait(control_input_stream, handle_control_input);
360 // Control socket closed: terminate
366 var registered_fds = 0;
370 onSocketAccepted: function (server, transport) {
371 unregistered_transports.push(transport);
372 function remove_from_unregistered() {
374 i = unregistered_transports.indexOf(transport);
376 unregistered_transports.splice(i, 1);
383 remove_from_unregistered();
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)
394 let remaining = header_size - received_data.length;
395 let avail = in_stream.available();
397 if (avail > remaining)
399 received_data += bin_stream.readBytes(avail);
401 if (received_data.length < header_size) {
402 input_stream_async_wait(in_stream, handle_input);
405 if (received_data.substring(0, key_length) != client_key)
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();
424 var fd = parseInt(fdspec);
425 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
429 registered_transports[fd] = {transport: transport,
434 if (control_transport && registered_fds == total_fds) {
436 control_state = CONTROL_SENDING_KEY;
437 async_binary_write(control_output_stream, server_key,
439 control_state = CONTROL_SENT_KEY;
441 setup_timer.cancel();
444 if (terminate_pending) {
445 control_send_terminate();
449 let t = registered_transports[i];
457 input_stream_async_wait(control_input_stream, handle_control_input);
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);
475 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
479 // Allow the exception to propagate to the caller
485 * spawn_process_blind: spawn a process and forget about it
487 define_keywords("$cwd", "$fds");
488 function spawn_process_blind(program_name, args) {
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));
496 spawn_process(program_name, args, cwd,
497 null /* success callback */,
498 null /* failure callback */,
504 // Keyword arguments: $cwd, $fds
505 function spawn_and_wait_for_process(program_name, args) {
507 var cc = yield CONTINUATION;
508 spawn_process(program_name, args, arguments.$cwd,
511 var result = yield SUSPEND;
512 yield co_return(result);
515 // Keyword arguments: $cwd, $fds
516 function shell_command_blind(cmd) {
518 /* Check if we can use spawn_process_internal */
519 var cwd = arguments.$cwd;
520 var fds = arguments.$fds;
528 full_cmd = "cd \"" + shell_quote(cwd) + "\"; " + cmd;
531 program_name = getenv("SHELL") || "/bin/sh";
532 args = [null, "-c", full_cmd];
537 if (cwd.match(/[a-z]:/i)) {
538 full_cmd += cwd.substring(0,2) + " && ";
540 full_cmd += "cd \"" + shell_quote(cwd) + "\" && " + 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"];
550 for (var i = 0; i < full_cmd.length; ++i) {
551 var ch = full_cmd[i];
567 if (cur_arg.length > 0)
569 program_name = "cmd.exe";
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) + "\"";
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) {
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))));