2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4 * Use, modification, and distribution are subject to the terms specified in the
8 require("interactive.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) {
23 var file = Cc["@mozilla.org/file/local;1"]
24 .createInstance(Ci.nsILocalFile);
25 if (! path_component_regexp.test(name)) {
28 file.initWithPath(name);
35 for (var i = 0, plen = PATH.length; i < plen; ++i) {
37 file.initWithPath(PATH[i]);
38 file.appendRelativePath(name);
47 function spawn_process_internal (program, args, blocking) {
48 var process = Cc["@mozilla.org/process/util;1"]
49 .createInstance(Ci.nsIProcess);
50 process.init(get_file_in_path(program));
51 return process.run(!!blocking, args, args.length);
54 var PATH_programs = null;
55 function get_shell_command_completer () {
56 if (PATH_programs == null) {
58 var file = Cc["@mozilla.org/file/local;1"]
59 .createInstance(Ci.nsILocalFile);
60 for (var i = 0, plen = PATH.length; i < plen; ++i) {
62 file.initWithPath(PATH[i]);
63 var entries = file.directoryEntries;
64 while (entries.hasMoreElements()) {
65 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
66 PATH_programs.push(entry.leafName);
72 return prefix_completer($completions = PATH_programs,
73 $get_string = function (x) x);
77 minibuffer_auto_complete_preferences["shell-command"] = null;
79 /* FIXME: support a relative or full path as well as PATH commands */
80 define_keywords("$cwd");
81 minibuffer.prototype.read_shell_command = function () {
82 keywords(arguments, $history = "shell-command");
83 var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd.path + "]:";
84 var result = yield this.read(
86 $history = "shell-command",
87 $auto_complete = "shell-command",
89 $validator = function (x, m) {
90 var s = x.replace(/^\s+|\s+$/g, '');
92 m.message("A blank shell command is not allowed.");
97 forward_keywords(arguments),
98 $completer = get_shell_command_completer());
99 yield co_return(result);
102 const STDIN_FILENO = 0;
103 const STDOUT_FILENO = 1;
104 const STDERR_FILENO = 2;
106 var spawn_process_helper_default_fd_wait_timeout = 1000;
107 var spawn_process_helper_setup_timeout = 2000;
108 var spawn_process_helper_program = file_locator_service.get("CurProcD", Ci.nsIFile);
109 spawn_process_helper_program.append("conkeror-spawn-helper");
112 * @param program_name
113 * Specifies the full path to the program.
115 * An array of strings to pass as the arguments to the program. The
116 * first argument should be the program name. These strings must not
117 * have any NUL bytes in them.
119 * If non-null, must be an nsILocalFile. spawn_process will switch
120 * to this path before running the program.
121 * @param finished_callback
122 * Called with a single argument, the exit code of the process, as
123 * returned by the wait system call.
124 * @param failure_callback
125 * Called with a single argument, an exception, if one occurs.
127 * If non-null, must be an object with only non-negative integer
128 * properties set. Each such property specifies that the corresponding
129 * file descriptor in the spawned process should be redirected. Note
130 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
131 * corresponds to STDERR. Note that every redirected file descriptor can
132 * be used for both input and output, although STDIN, STDOUT, and STDERR
133 * are typically used only unidirectionally. Each property must be an
134 * object itself, with an input and/or output property specifying
135 * callback functions that are called with an nsIAsyncInputStream or
136 * nsIAsyncOutputStream when the stream for that file descriptor is
138 * @param fd_wait_timeout
139 * Specifies the number of milliseconds to wait for the file descriptor
140 * redirection sockets to be closed after the control socket indicates
141 * the process has exited before they are closed forcefully. A negative
142 * value means to wait indefinitely. If fd_wait_timeout is null,
143 * spawn_process_helper_default_fd_wait_timeout is used instead.
145 * A function that can be called to prematurely terminate the spawned
148 function spawn_process (program_name, args, working_dir,
149 success_callback, failure_callback, fds,
153 args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
155 program_name = get_file_in_path(program_name).path;
157 const key_length = 100;
158 const fd_spec_size = 15;
163 if (fd_wait_timeout === undefined)
164 fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
166 var unregistered_transports = [];
167 var registered_transports = [];
170 var setup_timer = null;
172 const CONTROL_CONNECTED = 0;
173 const CONTROL_SENDING_KEY = 1;
174 const CONTROL_SENT_KEY = 2;
176 var control_state = CONTROL_CONNECTED;
177 var terminate_pending = false;
179 var control_transport = null;
181 var control_binary_input_stream = null;
182 var control_output_stream = null, control_input_stream = null;
183 var exit_status = null;
187 // Make sure key does not have any 0 bytes in it.
188 for (let i = 0; i < key_length; ++i)
189 client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
191 // Make sure key does not have any 0 bytes in it.
192 for (let i = 0; i < key_length; ++i)
193 server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
195 var key_file_fd_data = "";
197 // This is the total number of redirected file descriptors.
198 var total_client_fds = 0;
200 // This is the total number of redirected file descriptors that will use a socket connection.
204 if (fds.hasOwnProperty(i)) {
205 if (fds[i] == null) {
209 key_file_fd_data += i + "\0";
212 if (fd.perms == null)
214 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
215 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
218 key_file_fd_data += "\0";
223 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
224 (working_dir != null ? working_dir.path : "") + "\0" +
226 args.join("\0") + "\0" +
227 total_client_fds + "\0" + key_file_fd_data;
230 if (!terminate_pending) {
232 if (failure_callback)
237 function cleanup_server () {
242 for (let i in unregistered_transports) {
243 unregistered_transports[i].close(0);
244 delete unregistered_transports[i];
248 function cleanup_fd_sockets () {
249 for (let i in registered_transports) {
250 registered_transports[i].transport.close(0);
251 delete registered_transports[i];
255 function cleanup_control () {
256 if (control_transport) {
257 control_binary_input_stream.close();
258 control_binary_input_stream = null;
259 control_transport.close(0);
260 control_transport = null;
261 control_input_stream = null;
262 control_output_stream = null;
266 function control_send_terminate () {
267 control_input_stream = null;
268 control_binary_input_stream.close();
269 control_binary_input_stream = null;
270 async_binary_write(control_output_stream, "\0", function () {
271 control_output_stream = null;
272 control_transport.close(0);
273 control_transport = null;
277 function terminate () {
278 if (terminate_pending)
280 terminate_pending = true;
282 setup_timer.cancel();
286 cleanup_fd_sockets();
287 if (control_transport) {
288 switch (control_state) {
289 case CONTROL_SENT_KEY:
290 control_send_terminate();
292 case CONTROL_CONNECTED:
296 * case CONTROL_SENDING_KEY: in this case once the key
297 * is sent, the terminate_pending flag will be noticed
298 * and control_send_terminate will be called, so nothing
299 * more needs to be done here.
306 function finished () {
307 // Only call success_callback if terminate was not already called
308 if (!terminate_pending) {
310 if (success_callback)
311 success_callback(exit_status);
315 // Create server socket to listen for connections from the external helper program
317 server = Cc['@mozilla.org/network/server-socket;1']
318 .createInstance(Ci.nsIServerSocket);
320 var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
322 write_binary_file(key_file, key_file_data);
323 server.init(-1 /* choose a port automatically */,
324 true /* bind to localhost only */,
325 -1 /* select backlog size automatically */);
327 setup_timer = call_after_timeout(function () {
329 if (control_state != CONTROL_SENT_KEY)
330 fail("setup timeout");
331 }, spawn_process_helper_setup_timeout);
333 function wait_for_fd_sockets () {
334 var remaining_streams = total_fds * 2;
336 function handler () {
337 if (remaining_streams != null) {
339 if (remaining_streams == 0) {
346 for each (let f in registered_transports) {
347 input_stream_async_wait(f.input, handler, false /* wait for closure */);
348 output_stream_async_wait(f.output, handler, false /* wait for closure */);
350 if (fd_wait_timeout != null) {
351 timer = call_after_timeout(function() {
352 remaining_streams = null;
358 var control_data = "";
360 function handle_control_input () {
361 if (terminate_pending)
364 let avail = control_input_stream.available();
366 control_data += control_binary_input_stream.readBytes(avail);
367 var off = control_data.indexOf("\0");
369 let message = control_data.substring(0,off);
370 exit_status = parseInt(message);
372 /* wait for all fd sockets to close? */
374 wait_for_fd_sockets();
380 input_stream_async_wait(control_input_stream, handle_control_input);
382 // Control socket closed: terminate
388 var registered_fds = 0;
392 onSocketAccepted: function (server, transport) {
393 unregistered_transports.push(transport);
394 function remove_from_unregistered () {
396 i = unregistered_transports.indexOf(transport);
398 unregistered_transports.splice(i, 1);
405 remove_from_unregistered();
407 var received_data = "";
408 var header_size = key_length + fd_spec_size;
410 var in_stream, bin_stream, out_stream;
412 function handle_input () {
413 if (terminate_pending)
416 let remaining = header_size - received_data.length;
417 let avail = in_stream.available();
419 if (avail > remaining)
421 received_data += bin_stream.readBytes(avail);
423 if (received_data.length < header_size) {
424 input_stream_async_wait(in_stream, handle_input);
427 if (received_data.substring(0, key_length) != client_key)
434 var fdspec = received_data.substring(key_length);
435 if (fdspec.charCodeAt(0) == 0) {
437 // This is the control connection
438 if (control_transport)
439 throw "Control transport already exists";
440 control_transport = transport;
441 control_output_stream = out_stream;
442 control_input_stream = in_stream;
443 control_binary_input_stream = bin_stream;
444 remove_from_unregistered();
446 var fd = parseInt(fdspec);
447 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
449 remove_from_unregistered();
451 registered_transports[fd] = {transport: transport,
456 if (control_transport && registered_fds == total_fds) {
458 control_state = CONTROL_SENDING_KEY;
459 async_binary_write(control_output_stream, server_key,
463 control_state = CONTROL_SENT_KEY;
465 setup_timer.cancel();
468 if (terminate_pending) {
469 control_send_terminate();
473 let t = registered_transports[i];
485 input_stream_async_wait(control_input_stream, handle_control_input);
493 in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
494 out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
495 bin_stream = binary_input_stream(in_stream);
496 input_stream_async_wait(in_stream, handle_input);
501 onStopListening: function (s, status) {
505 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
510 if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
512 throw new Error("Error spawning process: not yet supported on MS Windows");
514 throw new Error("Error spawning process: conkeror-spawn-helper not found; try running \"make\"");
516 // Allow the exception to propagate to the caller
522 * spawn_process_blind: spawn a process and forget about it
524 define_keywords("$cwd", "$fds");
525 function spawn_process_blind (program_name, args) {
527 /* Check if we can use spawn_process_internal */
528 var cwd = arguments.$cwd;
529 var fds = arguments.$fds;
530 if (cwd == null && fds == null && args[0] == null)
531 spawn_process_internal(program_name, args.slice(1));
533 spawn_process(program_name, args, cwd,
534 null /* success callback */,
535 null /* failure callback */,
541 // Keyword arguments: $cwd, $fds
542 function spawn_and_wait_for_process (program_name, args) {
544 var cc = yield CONTINUATION;
545 spawn_process(program_name, args, arguments.$cwd,
548 var result = yield SUSPEND;
549 yield co_return(result);
552 // Keyword arguments: $cwd, $fds
553 function shell_command_blind (cmd) {
555 /* Check if we can use spawn_process_internal */
556 var cwd = arguments.$cwd;
557 var fds = arguments.$fds;
565 full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
568 program_name = getenv("SHELL") || "/bin/sh";
569 args = [null, "-c", full_cmd];
574 if (cwd.path.match(/[a-z]:/i)) {
575 full_cmd += cwd.path.substring(0,2) + " && ";
577 full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
581 /* Need to convert the single command-line into a list of
582 * arguments that will then get converted back into a *
583 command-line by Mozilla. */
584 var out = [null, "/C"];
587 for (var i = 0; i < full_cmd.length; ++i) {
588 var ch = full_cmd[i];
604 if (cur_arg.length > 0)
606 program_name = "cmd.exe";
609 spawn_process_blind(program_name, args, $fds = arguments.$fds);
612 function substitute_shell_command_argument (cmdline, argument) {
613 if (!cmdline.match("{}"))
614 return cmdline + " \"" + shell_quote(argument) + "\"";
616 return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
619 function shell_command_with_argument_blind (command, arg) {
620 shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
625 * $cwd: The current working directory for the process.
626 * $fds: File descriptors to use.
628 function shell_command (command) {
630 throw new Error("shell_command: Your OS is not yet supported");
631 var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
632 [null, "-c", command],
633 forward_keywords(arguments));
634 yield co_return(result);
637 function shell_command_with_argument (command, arg) {
638 yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));