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"].createInstance(Ci.nsILocalFile);
24 if (!path_component_regexp.test(name)) {
27 file.initWithPath(name);
34 for (var i = 0; i < PATH.length; ++i) {
36 file.initWithPath(PATH[i]);
37 file.appendRelativePath(name);
46 function spawn_process_internal(program, args, blocking) {
47 var process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
48 process.init(get_file_in_path(program));
49 return process.run(!!blocking, args, args.length);
52 var PATH_programs = null;
53 function get_shell_command_completer() {
54 if (PATH_programs == null) {
56 var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
57 for (var i = 0; i < PATH.length; ++i) {
59 file.initWithPath(PATH[i]);
60 var entries = file.directoryEntries;
61 while (entries.hasMoreElements()) {
62 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
63 PATH_programs.push(entry.leafName);
70 return prefix_completer($completions = PATH_programs,
71 $get_string = function (x) { return x; });
75 minibuffer_auto_complete_preferences["shell-command"] = null;
77 /* FIXME: support a relative or full path as well as PATH commands */
78 define_keywords("$cwd");
79 minibuffer.prototype.read_shell_command = function () {
80 keywords(arguments, $history = "shell-command");
81 var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd + "]:";
82 var result = yield this.read(
84 $history = "shell-command",
85 $auto_complete = "shell-command",
87 $validator = function (x, m) {
88 var s = x.replace(/^\s+|\s+$/g, '');
90 m.message("A blank shell command is not allowed.");
95 forward_keywords(arguments),
96 $completer = get_shell_command_completer());
97 yield co_return(result);
100 const STDIN_FILENO = 0;
101 const STDOUT_FILENO = 1;
102 const STDERR_FILENO = 2;
104 var spawn_process_helper_default_fd_wait_timeout = 1000;
105 var spawn_process_helper_setup_timeout = 2000;
106 var spawn_process_helper_program = file_locator.get("CurProcD", Ci.nsIFile);
107 spawn_process_helper_program.append("conkeror-spawn-helper");
110 * @param program_name
111 * Specifies the full path to the program.
113 * An array of strings to pass as the arguments to the program. The
114 * first argument should be the program name. These strings must not
115 * have any NUL bytes in them.
117 * If non-null, switch to the specified path before running the program.
118 * @param finished_callback
119 * Called with a single argument, the exit code of the process, as
120 * returned by the wait system call.
121 * @param failure_callback
122 * Called with a single argument, an exception, if one occurs.
124 * If non-null, must be an object with only non-negative integer
125 * properties set. Each such property specifies that the corresponding
126 * file descriptor in the spawned process should be redirected. Note
127 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
128 * corresponds to STDERR. Note that every redirected file descriptor can
129 * be used for both input and output, although STDIN, STDOUT, and STDERR
130 * are typically used only unidirectionally. Each property must be an
131 * object itself, with an input and/or output property specifying
132 * callback functions that are called with an nsIAsyncInputStream or
133 * nsIAsyncOutputStream when the stream for that file descriptor is
135 * @param fd_wait_timeout
136 * Specifies the number of milliseconds to wait for the file descriptor
137 * redirection sockets to be closed after the control socket indicates
138 * the process has exited before they are closed forcefully. A negative
139 * value means to wait indefinitely. If fd_wait_timeout is null,
140 * spawn_process_helper_default_fd_wait_timeout is used instead.
142 * A function that can be called to prematurely terminate the spawned
145 function spawn_process(program_name, args, working_dir,
146 success_callback, failure_callback, fds,
151 args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
153 program_name = get_file_in_path(program_name).path;
155 const key_length = 100;
156 const fd_spec_size = 15;
161 if (fd_wait_timeout === undefined)
162 fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
164 var unregistered_transports = [];
165 var registered_transports = [];
168 var setup_timer = null;
170 const CONTROL_CONNECTED = 0;
171 const CONTROL_SENDING_KEY = 1;
172 const CONTROL_SENT_KEY = 2;
174 var control_state = CONTROL_CONNECTED;
175 var terminate_pending = false;
177 var control_transport = null;
179 var control_binary_input_stream = null;
180 var control_output_stream = null, control_input_stream = null;
181 var exit_status = null;
185 // Make sure key does not have any 0 bytes in it.
186 for (let i = 0; i < key_length; ++i) client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
188 // Make sure key does not have any 0 bytes in it.
189 for (let i = 0; i < key_length; ++i) server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
191 var key_file_fd_data = "";
193 // This is the total number of redirected file descriptors.
194 var total_client_fds = 0;
196 // This is the total number of redirected file descriptors that will use a socket connection.
200 if (fds.hasOwnProperty(i)) {
201 if (fds[i] == null) {
205 key_file_fd_data += i + "\0";
208 if (fd.perms == null)
210 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
211 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
214 key_file_fd_data += "\0";
219 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
220 (working_dir != null ? working_dir : "") + "\0" +
222 args.join("\0") + "\0" +
223 total_client_fds + "\0" + key_file_fd_data;
226 if (!terminate_pending) {
228 if (failure_callback)
233 function cleanup_server() {
238 for (let i in unregistered_transports) {
239 unregistered_transports[i].close(0);
240 delete unregistered_transports[i];
244 function cleanup_fd_sockets() {
245 for (let i in registered_transports) {
246 registered_transports[i].transport.close(0);
247 delete registered_transports[i];
251 function cleanup_control() {
252 if (control_transport) {
253 control_binary_input_stream.close();
254 control_binary_input_stream = null;
255 control_transport.close(0);
256 control_transport = null;
257 control_input_stream = null;
258 control_output_stream = null;
262 function control_send_terminate() {
263 control_input_stream = null;
264 control_binary_input_stream.close();
265 control_binary_input_stream = null;
266 async_binary_write(control_output_stream, "\0", function () {
267 control_output_stream = null;
268 control_transport.close(0);
269 control_transport = null;
273 function terminate() {
274 if (terminate_pending)
276 terminate_pending = true;
278 setup_timer.cancel();
282 cleanup_fd_sockets();
283 if (control_transport) {
284 switch (control_state) {
285 case CONTROL_SENT_KEY:
286 control_send_terminate();
288 case CONTROL_CONNECTED:
292 * case CONTROL_SENDING_KEY: in this case once the key
293 * is sent, the terminate_pending flag will be noticed
294 * and control_send_terminate will be called, so nothing
295 * more needs to be done here.
302 function finished() {
303 // Only call success_callback if terminate was not already called
304 if (!terminate_pending) {
306 if (success_callback)
307 success_callback(exit_status);
311 // Create server socket to listen for connections from the external helper program
313 server = Cc['@mozilla.org/network/server-socket;1'].createInstance(Ci.nsIServerSocket);
315 var key_file = get_temporary_file("spawn_process_key.dat");
317 write_binary_file(key_file, key_file_data);
318 server.init(-1 /* choose a port automatically */,
319 true /* bind to localhost only */,
320 -1 /* select backlog size automatically */);
322 setup_timer = call_after_timeout(function () {
324 if (control_state != CONTROL_SENT_KEY)
325 fail("setup timeout");
326 }, spawn_process_helper_setup_timeout);
329 function wait_for_fd_sockets() {
330 var remaining_streams = total_fds * 2;
333 if (remaining_streams != null) {
335 if (remaining_streams == 0) {
342 for each (let f in registered_transports) {
343 input_stream_async_wait(f.input, handler, false /* wait for closure */);
344 output_stream_async_wait(f.output, handler, false /* wait for closure */);
346 if (fd_wait_timeout != null) {
347 timer = call_after_timeout(function() {
348 remaining_streams = null;
354 var control_data = "";
356 function handle_control_input() {
357 if (terminate_pending)
360 let avail = control_input_stream.available();
362 control_data += control_binary_input_stream.readBytes(avail);
363 var off = control_data.indexOf("\0");
365 let message = control_data.substring(0,off);
366 exit_status = parseInt(message);
368 /* wait for all fd sockets to close? */
370 wait_for_fd_sockets();
376 input_stream_async_wait(control_input_stream, handle_control_input);
378 // Control socket closed: terminate
384 var registered_fds = 0;
388 onSocketAccepted: function (server, transport) {
389 unregistered_transports.push(transport);
390 function remove_from_unregistered() {
392 i = unregistered_transports.indexOf(transport);
394 unregistered_transports.splice(i, 1);
401 remove_from_unregistered();
403 var received_data = "";
404 var header_size = key_length + fd_spec_size;
406 var in_stream, bin_stream, out_stream;
408 function handle_input() {
409 if (terminate_pending)
412 let remaining = header_size - received_data.length;
413 let avail = in_stream.available();
415 if (avail > remaining)
417 received_data += bin_stream.readBytes(avail);
419 if (received_data.length < header_size) {
420 input_stream_async_wait(in_stream, handle_input);
423 if (received_data.substring(0, key_length) != client_key)
430 var fdspec = received_data.substring(key_length);
431 if (fdspec.charCodeAt(0) == 0) {
433 // This is the control connection
434 if (control_transport)
435 throw "Control transport already exists";
436 control_transport = transport;
437 control_output_stream = out_stream;
438 control_input_stream = in_stream;
439 control_binary_input_stream = bin_stream;
440 remove_from_unregistered();
442 var fd = parseInt(fdspec);
443 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
445 remove_from_unregistered();
447 registered_transports[fd] = {transport: transport,
452 if (control_transport && registered_fds == total_fds) {
454 control_state = CONTROL_SENDING_KEY;
455 async_binary_write(control_output_stream, server_key,
459 control_state = CONTROL_SENT_KEY;
461 setup_timer.cancel();
464 if (terminate_pending) {
465 control_send_terminate();
469 let t = registered_transports[i];
481 input_stream_async_wait(control_input_stream, handle_control_input);
489 in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
490 out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
491 bin_stream = binary_input_stream(in_stream);
492 input_stream_async_wait(in_stream, handle_input);
497 onStopListening: function (s, status) {
501 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
506 if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
508 throw new Error("Error spawning process: not yet supported on MS Windows");
510 throw new Error("Error spawning process: conkeror-spawn-helper not found; try running \"make\"");
512 // Allow the exception to propagate to the caller
518 * spawn_process_blind: spawn a process and forget about it
520 define_keywords("$cwd", "$fds");
521 function spawn_process_blind(program_name, args) {
523 /* Check if we can use spawn_process_internal */
524 var cwd = arguments.$cwd;
525 var fds = arguments.$fds;
526 if (cwd == null && fds == null && args[0] == null)
527 spawn_process_internal(program_name, args.slice(1));
529 spawn_process(program_name, args, cwd,
530 null /* success callback */,
531 null /* failure callback */,
537 // Keyword arguments: $cwd, $fds
538 function spawn_and_wait_for_process(program_name, args) {
540 var cc = yield CONTINUATION;
541 spawn_process(program_name, args, arguments.$cwd,
544 var result = yield SUSPEND;
545 yield co_return(result);
548 // Keyword arguments: $cwd, $fds
549 function shell_command_blind(cmd) {
551 /* Check if we can use spawn_process_internal */
552 var cwd = arguments.$cwd;
553 var fds = arguments.$fds;
561 full_cmd = "cd \"" + shell_quote(cwd) + "\"; " + cmd;
564 program_name = getenv("SHELL") || "/bin/sh";
565 args = [null, "-c", full_cmd];
570 if (cwd.match(/[a-z]:/i)) {
571 full_cmd += cwd.substring(0,2) + " && ";
573 full_cmd += "cd \"" + shell_quote(cwd) + "\" && " + cmd;
577 /* Need to convert the single command-line into a list of
578 * arguments that will then get converted back into a *
579 command-line by Mozilla. */
580 var out = [null, "/C"];
583 for (var i = 0; i < full_cmd.length; ++i) {
584 var ch = full_cmd[i];
600 if (cur_arg.length > 0)
602 program_name = "cmd.exe";
605 spawn_process_blind(program_name, args, $fds = arguments.$fds);
608 function substitute_shell_command_argument(cmdline, argument) {
609 if (!cmdline.match("{}"))
610 return cmdline + " \"" + shell_quote(argument) + "\"";
612 return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
615 function shell_command_with_argument_blind(command, arg) {
616 shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
621 * $cwd: The current working directory for the process.
622 * $fds: File descriptors to use.
624 function shell_command(command) {
626 throw new Error("shell_command: Your OS is not yet supported");
627 var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
628 [null, "-c", command],
629 forward_keywords(arguments));
630 yield co_return(result);
633 function shell_command_with_argument(command, arg) {
634 yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));