2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4 * Use, modification, and distribution are subject to the terms specified in the
10 require("interactive.js");
13 const WINDOWS = (get_os() == "WINNT");
14 const POSIX = !WINDOWS;
15 const PATH = getenv("PATH").split(POSIX ? ":" : ";");
17 const path_component_regexp = POSIX ? /^[^\/]+$/ : /^[^\/\\]+$/;
19 function get_file_in_path (name) {
20 if (name instanceof Ci.nsIFile) {
25 var file = Cc["@mozilla.org/file/local;1"]
26 .createInstance(Ci.nsILocalFile);
27 if (! path_component_regexp.test(name)) {
30 file.initWithPath(name);
37 for (var i = 0, plen = PATH.length; i < plen; ++i) {
39 file.initWithPath(PATH[i]);
40 file.appendRelativePath(name);
49 function spawn_process_internal (program, args, blocking) {
50 var process = Cc["@mozilla.org/process/util;1"]
51 .createInstance(Ci.nsIProcess);
52 process.init(get_file_in_path(program));
53 return process.run(!!blocking, args, args.length);
56 var PATH_programs = null;
57 function get_shell_command_completer () {
58 if (PATH_programs == null) {
60 var file = Cc["@mozilla.org/file/local;1"]
61 .createInstance(Ci.nsILocalFile);
62 for (var i = 0, plen = PATH.length; i < plen; ++i) {
64 file.initWithPath(PATH[i]);
65 var entries = file.directoryEntries;
66 while (entries.hasMoreElements()) {
67 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
68 PATH_programs.push(entry.leafName);
74 return prefix_completer($completions = PATH_programs,
75 $get_string = function (x) x);
79 minibuffer_auto_complete_preferences["shell-command"] = null;
81 /* FIXME: support a relative or full path as well as PATH commands */
82 define_keywords("$cwd");
83 minibuffer.prototype.read_shell_command = function () {
84 keywords(arguments, $history = "shell-command");
85 var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd.path + "]:";
86 var result = yield this.read(
88 $history = "shell-command",
89 $auto_complete = "shell-command",
91 $validator = function (x, m) {
92 var s = x.replace(/^\s+|\s+$/g, '');
94 m.message("A blank shell command is not allowed.");
99 forward_keywords(arguments),
100 $completer = get_shell_command_completer());
101 yield co_return(result);
104 const STDIN_FILENO = 0;
105 const STDOUT_FILENO = 1;
106 const STDERR_FILENO = 2;
108 var spawn_process_helper_default_fd_wait_timeout = 1000;
109 var spawn_process_helper_setup_timeout = 2000;
110 var spawn_process_helper_program = file_locator_service.get("CurProcD", Ci.nsIFile);
111 spawn_process_helper_program.append("conkeror-spawn-helper");
114 * @param program_name
115 * Specifies the full path to the program.
117 * An array of strings to pass as the arguments to the program. The
118 * first argument should be the program name. These strings must not
119 * have any NUL bytes in them.
121 * If non-null, must be an nsILocalFile. spawn_process will switch
122 * to this path before running the program.
123 * @param finished_callback
124 * Called with a single argument, the exit code of the process, as
125 * returned by the wait system call.
126 * @param failure_callback
127 * Called with a single argument, an exception, if one occurs.
129 * If non-null, must be an object with only non-negative integer
130 * properties set. Each such property specifies that the corresponding
131 * file descriptor in the spawned process should be redirected. Note
132 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
133 * corresponds to STDERR. Note that every redirected file descriptor can
134 * be used for both input and output, although STDIN, STDOUT, and STDERR
135 * are typically used only unidirectionally. Each property must be an
136 * object itself, with an input and/or output property specifying
137 * callback functions that are called with an nsIAsyncInputStream or
138 * nsIAsyncOutputStream when the stream for that file descriptor is
140 * @param fd_wait_timeout
141 * Specifies the number of milliseconds to wait for the file descriptor
142 * redirection sockets to be closed after the control socket indicates
143 * the process has exited before they are closed forcefully. A negative
144 * value means to wait indefinitely. If fd_wait_timeout is null,
145 * spawn_process_helper_default_fd_wait_timeout is used instead.
147 * A function that can be called to prematurely terminate the spawned
150 function spawn_process (program_name, args, working_dir,
151 success_callback, failure_callback, fds,
155 args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
157 program_name = get_file_in_path(program_name).path;
159 const key_length = 100;
160 const fd_spec_size = 15;
165 if (fd_wait_timeout === undefined)
166 fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
168 var unregistered_transports = [];
169 var registered_transports = [];
172 var setup_timer = null;
174 const CONTROL_CONNECTED = 0;
175 const CONTROL_SENDING_KEY = 1;
176 const CONTROL_SENT_KEY = 2;
178 var control_state = CONTROL_CONNECTED;
179 var terminate_pending = false;
181 var control_transport = null;
183 var control_binary_input_stream = null;
184 var control_output_stream = null, control_input_stream = null;
185 var exit_status = null;
189 // Make sure key does not have any 0 bytes in it.
190 for (let i = 0; i < key_length; ++i)
191 client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
193 // Make sure key does not have any 0 bytes in it.
194 for (let i = 0; i < key_length; ++i)
195 server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
197 var key_file_fd_data = "";
199 // This is the total number of redirected file descriptors.
200 var total_client_fds = 0;
202 // This is the total number of redirected file descriptors that will use a socket connection.
206 if (fds.hasOwnProperty(i)) {
207 if (fds[i] == null) {
211 key_file_fd_data += i + "\0";
214 if (fd.perms == null)
216 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
217 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
220 key_file_fd_data += "\0";
225 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
226 (working_dir != null ? working_dir.path : "") + "\0" +
228 args.join("\0") + "\0" +
229 total_client_fds + "\0" + key_file_fd_data;
232 if (!terminate_pending) {
234 if (failure_callback)
239 function cleanup_server () {
244 for (let i in unregistered_transports) {
245 unregistered_transports[i].close(0);
246 delete unregistered_transports[i];
250 function cleanup_fd_sockets () {
251 for (let i in registered_transports) {
252 registered_transports[i].transport.close(0);
253 delete registered_transports[i];
257 function cleanup_control () {
258 if (control_transport) {
259 control_binary_input_stream.close();
260 control_binary_input_stream = null;
261 control_transport.close(0);
262 control_transport = null;
263 control_input_stream = null;
264 control_output_stream = null;
268 function control_send_terminate () {
269 control_input_stream = null;
270 control_binary_input_stream.close();
271 control_binary_input_stream = null;
272 async_binary_write(control_output_stream, "\0", function () {
273 control_output_stream = null;
274 control_transport.close(0);
275 control_transport = null;
279 function terminate () {
280 if (terminate_pending)
282 terminate_pending = true;
284 setup_timer.cancel();
288 cleanup_fd_sockets();
289 if (control_transport) {
290 switch (control_state) {
291 case CONTROL_SENT_KEY:
292 control_send_terminate();
294 case CONTROL_CONNECTED:
298 * case CONTROL_SENDING_KEY: in this case once the key
299 * is sent, the terminate_pending flag will be noticed
300 * and control_send_terminate will be called, so nothing
301 * more needs to be done here.
308 function finished () {
309 // Only call success_callback if terminate was not already called
310 if (!terminate_pending) {
312 if (success_callback)
313 success_callback(exit_status);
317 // Create server socket to listen for connections from the external helper program
319 server = Cc['@mozilla.org/network/server-socket;1']
320 .createInstance(Ci.nsIServerSocket);
322 var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
324 write_binary_file(key_file, key_file_data);
325 server.init(-1 /* choose a port automatically */,
326 true /* bind to localhost only */,
327 -1 /* select backlog size automatically */);
329 setup_timer = call_after_timeout(function () {
331 if (control_state != CONTROL_SENT_KEY)
332 fail("setup timeout");
333 }, spawn_process_helper_setup_timeout);
335 function wait_for_fd_sockets () {
336 var remaining_streams = total_fds * 2;
338 function handler () {
339 if (remaining_streams != null) {
341 if (remaining_streams == 0) {
348 for each (let f in registered_transports) {
349 input_stream_async_wait(f.input, handler, false /* wait for closure */);
350 output_stream_async_wait(f.output, handler, false /* wait for closure */);
352 if (fd_wait_timeout != null) {
353 timer = call_after_timeout(function() {
354 remaining_streams = null;
360 var control_data = "";
362 function handle_control_input () {
363 if (terminate_pending)
366 let avail = control_input_stream.available();
368 control_data += control_binary_input_stream.readBytes(avail);
369 var off = control_data.indexOf("\0");
371 let message = control_data.substring(0,off);
372 exit_status = parseInt(message);
374 /* wait for all fd sockets to close? */
376 wait_for_fd_sockets();
382 input_stream_async_wait(control_input_stream, handle_control_input);
384 // Control socket closed: terminate
390 var registered_fds = 0;
394 onSocketAccepted: function (server, transport) {
395 unregistered_transports.push(transport);
396 function remove_from_unregistered () {
398 i = unregistered_transports.indexOf(transport);
400 unregistered_transports.splice(i, 1);
407 remove_from_unregistered();
409 var received_data = "";
410 var header_size = key_length + fd_spec_size;
412 var in_stream, bin_stream, out_stream;
414 function handle_input () {
415 if (terminate_pending)
418 let remaining = header_size - received_data.length;
419 let avail = in_stream.available();
421 if (avail > remaining)
423 received_data += bin_stream.readBytes(avail);
425 if (received_data.length < header_size) {
426 input_stream_async_wait(in_stream, handle_input);
429 if (received_data.substring(0, key_length) != client_key)
436 var fdspec = received_data.substring(key_length);
437 if (fdspec.charCodeAt(0) == 0) {
439 // This is the control connection
440 if (control_transport)
441 throw "Control transport already exists";
442 control_transport = transport;
443 control_output_stream = out_stream;
444 control_input_stream = in_stream;
445 control_binary_input_stream = bin_stream;
446 remove_from_unregistered();
448 var fd = parseInt(fdspec);
449 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
451 remove_from_unregistered();
453 registered_transports[fd] = {transport: transport,
458 if (control_transport && registered_fds == total_fds) {
460 control_state = CONTROL_SENDING_KEY;
461 async_binary_write(control_output_stream, server_key,
465 control_state = CONTROL_SENT_KEY;
467 setup_timer.cancel();
470 if (terminate_pending) {
471 control_send_terminate();
475 let t = registered_transports[i];
487 input_stream_async_wait(control_input_stream, handle_control_input);
495 in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
496 out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
497 bin_stream = binary_input_stream(in_stream);
498 input_stream_async_wait(in_stream, handle_input);
503 onStopListening: function (s, status) {
507 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
512 if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
514 throw new Error("Error spawning process: not yet supported on MS Windows");
516 throw new Error("Error spawning process: conkeror-spawn-helper not found; try running \"make\"");
518 // Allow the exception to propagate to the caller
524 * spawn_process_blind: spawn a process and forget about it
526 define_keywords("$cwd", "$fds");
527 function spawn_process_blind (program_name, args) {
529 /* Check if we can use spawn_process_internal */
530 var cwd = arguments.$cwd;
531 var fds = arguments.$fds;
532 if (cwd == null && fds == null && args[0] == null)
533 spawn_process_internal(program_name, args.slice(1));
535 spawn_process(program_name, args, cwd,
536 null /* success callback */,
537 null /* failure callback */,
543 // Keyword arguments: $cwd, $fds
544 function spawn_and_wait_for_process (program_name, args) {
545 keywords(arguments, $cwd = null, $fds = null);
546 var cc = yield CONTINUATION;
547 spawn_process(program_name, args, arguments.$cwd,
550 var result = yield SUSPEND;
551 yield co_return(result);
554 // Keyword arguments: $cwd, $fds
555 function shell_command_blind (cmd) {
557 /* Check if we can use spawn_process_internal */
558 var cwd = arguments.$cwd;
559 var fds = arguments.$fds;
567 full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
570 program_name = getenv("SHELL") || "/bin/sh";
571 args = [null, "-c", full_cmd];
576 if (cwd.path.match(/[a-z]:/i)) {
577 full_cmd += cwd.path.substring(0,2) + " && ";
579 full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
583 /* Need to convert the single command-line into a list of
584 * arguments that will then get converted back into a *
585 command-line by Mozilla. */
586 var out = [null, "/C"];
589 for (var i = 0; i < full_cmd.length; ++i) {
590 var ch = full_cmd[i];
606 if (cur_arg.length > 0)
608 program_name = "cmd.exe";
611 spawn_process_blind(program_name, args, $fds = arguments.$fds);
614 function substitute_shell_command_argument (cmdline, argument) {
615 if (!cmdline.match("{}"))
616 return cmdline + " \"" + shell_quote(argument) + "\"";
618 return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
621 function shell_command_with_argument_blind (command, arg) {
622 shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
627 * $cwd: The current working directory for the process.
628 * $fds: File descriptors to use.
630 function shell_command (command) {
632 throw new Error("shell_command: Your OS is not yet supported");
633 var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
634 [null, "-c", command],
635 forward_keywords(arguments));
636 yield co_return(result);
639 function shell_command_with_argument (command, arg) {
640 yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));
643 provide("spawn-process");