2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2012 John J. Foerch
5 * Use, modification, and distribution are subject to the terms specified in the
9 require("interactive.js");
13 function spawn_process_internal (program, args, blocking) {
14 var process = Cc["@mozilla.org/process/util;1"]
15 .createInstance(Ci.nsIProcess);
16 process.init(find_file_in_path(program));
17 return process.run(!!blocking, args, args.length);
20 var PATH_programs = null;
22 function shell_command_completer () {
23 if (PATH_programs == null) {
25 var file = Cc["@mozilla.org/file/local;1"]
26 .createInstance(Ci.nsILocalFile);
27 for (var i = 0, plen = PATH.length; i < plen; ++i) {
29 file.initWithPath(PATH[i]);
30 var entries = file.directoryEntries;
31 while (entries.hasMoreElements()) {
32 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
33 PATH_programs.push(entry.leafName);
39 prefix_completer.call(this, $completions = PATH_programs);
41 shell_command_completer.prototype = {
42 constructor: shell_command_completer,
43 __proto__: prefix_completer.prototype,
44 toString: function () "#<shell_command_completer>"
48 minibuffer_auto_complete_preferences["shell-command"] = null;
50 /* FIXME: support a relative or full path as well as PATH commands */
51 define_keywords("$cwd");
52 minibuffer.prototype.read_shell_command = function () {
53 keywords(arguments, $history = "shell-command");
54 var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd.path + "]:";
55 var result = yield this.read(
57 $history = "shell-command",
58 $auto_complete = "shell-command",
60 $validator = function (x, m) {
61 var s = x.replace(/^\s+|\s+$/g, '');
63 m.message("A blank shell command is not allowed.");
68 forward_keywords(arguments),
69 $completer = new shell_command_completer());
70 yield co_return(result);
73 function find_spawn_helper () {
74 var f = file_locator_service.get("CurProcD", Ci.nsIFile);
75 f.append("conkeror-spawn-helper");
78 return find_file_in_path("conkeror-spawn-helper");
81 const STDIN_FILENO = 0;
82 const STDOUT_FILENO = 1;
83 const STDERR_FILENO = 2;
85 var spawn_process_helper_default_fd_wait_timeout = 1000;
86 var spawn_process_helper_setup_timeout = 2000;
90 * Specifies the full path to the program.
92 * An array of strings to pass as the arguments to the program. The
93 * first argument should be the program name. These strings must not
94 * have any NUL bytes in them.
96 * If non-null, must be an nsILocalFile. spawn_process will switch
97 * to this path before running the program.
98 * @param finished_callback
99 * Called with a single argument, the exit code of the process, as
100 * returned by the wait system call.
101 * @param failure_callback
102 * Called with a single argument, an exception, if one occurs.
104 * If non-null, must be an object with only non-negative integer
105 * properties set. Each such property specifies that the corresponding
106 * file descriptor in the spawned process should be redirected. Note
107 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
108 * corresponds to STDERR. Note that every redirected file descriptor can
109 * be used for both input and output, although STDIN, STDOUT, and STDERR
110 * are typically used only unidirectionally. Each property must be an
111 * object itself, with an input and/or output property specifying
112 * callback functions that are called with an nsIAsyncInputStream or
113 * nsIAsyncOutputStream when the stream for that file descriptor is
115 * @param fd_wait_timeout
116 * Specifies the number of milliseconds to wait for the file descriptor
117 * redirection sockets to be closed after the control socket indicates
118 * the process has exited before they are closed forcefully. A negative
119 * value means to wait indefinitely. If fd_wait_timeout is null,
120 * spawn_process_helper_default_fd_wait_timeout is used instead.
122 * A function that can be called to prematurely terminate the spawned
125 function spawn_process (program_name, args, working_dir,
126 fds, fd_wait_timeout) {
128 let deferred = Promise.defer();
130 var spawn_process_helper_program = find_spawn_helper();
131 if (spawn_process_helper_program == null)
132 throw new Error("Error spawning process: conkeror-spawn-helper not found");
135 args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
137 program_name = find_file_in_path(program_name).path;
139 const key_length = 100;
140 const fd_spec_size = 15;
145 if (fd_wait_timeout === undefined)
146 fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
148 var unregistered_transports = [];
149 var registered_transports = [];
152 var setup_timer = null;
154 const CONTROL_CONNECTED = 0;
155 const CONTROL_SENDING_KEY = 1;
156 const CONTROL_SENT_KEY = 2;
158 var control_state = CONTROL_CONNECTED;
159 var terminate_pending = false;
161 var control_transport = null;
163 var control_binary_input_stream = null;
164 var control_output_stream = null, control_input_stream = null;
165 var exit_status = null;
169 // Make sure key does not have any 0 bytes in it.
170 for (let i = 0; i < key_length; ++i)
171 client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
173 // Make sure key does not have any 0 bytes in it.
174 for (let i = 0; i < key_length; ++i)
175 server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
177 var key_file_fd_data = "";
179 // This is the total number of redirected file descriptors.
180 var total_client_fds = 0;
182 // This is the total number of redirected file descriptors that will use a socket connection.
186 if (fds.hasOwnProperty(i)) {
187 if (fds[i] == null) {
191 key_file_fd_data += i + "\0";
194 if (fd.perms == null)
196 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
197 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
200 key_file_fd_data += "\0";
205 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
206 (working_dir != null ? working_dir.path : "") + "\0" +
208 args.join("\0") + "\0" +
209 total_client_fds + "\0" + key_file_fd_data;
212 if (!terminate_pending) {
218 function cleanup_server () {
223 for (let i in unregistered_transports) {
224 unregistered_transports[i].close(0);
225 delete unregistered_transports[i];
229 function cleanup_fd_sockets () {
230 for (let i in registered_transports) {
231 registered_transports[i].transport.close(0);
232 delete registered_transports[i];
236 function cleanup_control () {
237 if (control_transport) {
238 control_binary_input_stream.close();
239 control_binary_input_stream = null;
240 control_transport.close(0);
241 control_transport = null;
242 control_input_stream = null;
243 control_output_stream = null;
247 function control_send_terminate () {
248 control_input_stream = null;
249 control_binary_input_stream.close();
250 control_binary_input_stream = null;
251 async_binary_write(control_output_stream, "\0", function () {
252 control_output_stream = null;
253 control_transport.close(0);
254 control_transport = null;
258 function terminate () {
259 if (terminate_pending)
261 terminate_pending = true;
263 setup_timer.cancel();
267 cleanup_fd_sockets();
268 if (control_transport) {
269 switch (control_state) {
270 case CONTROL_SENT_KEY:
271 control_send_terminate();
273 case CONTROL_CONNECTED:
277 * case CONTROL_SENDING_KEY: in this case once the key
278 * is sent, the terminate_pending flag will be noticed
279 * and control_send_terminate will be called, so nothing
280 * more needs to be done here.
287 function canceler (e) {
288 if (!terminate_pending) {
294 function finished () {
295 // Only call success_callback if terminate was not already called
296 if (!terminate_pending) {
297 deferred.resolve(exit_status);
302 // Create server socket to listen for connections from the external helper program
304 server = Cc['@mozilla.org/network/server-socket;1']
305 .createInstance(Ci.nsIServerSocket);
307 var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
309 write_binary_file(key_file, key_file_data);
310 server.init(-1 /* choose a port automatically */,
311 true /* bind to localhost only */,
312 -1 /* select backlog size automatically */);
314 setup_timer = call_after_timeout(function () {
316 if (control_state != CONTROL_SENT_KEY)
317 fail("setup timeout");
318 }, spawn_process_helper_setup_timeout);
320 function wait_for_fd_sockets () {
321 var remaining_streams = total_fds * 2;
323 function handler () {
324 if (remaining_streams != null) {
326 if (remaining_streams == 0) {
333 for each (let f in registered_transports) {
334 input_stream_async_wait(f.input, handler, false /* wait for closure */);
335 output_stream_async_wait(f.output, handler, false /* wait for closure */);
337 if (fd_wait_timeout != null) {
338 timer = call_after_timeout(function() {
339 remaining_streams = null;
345 var control_data = "";
347 function handle_control_input () {
348 if (terminate_pending)
351 let avail = control_input_stream.available();
353 control_data += control_binary_input_stream.readBytes(avail);
354 var off = control_data.indexOf("\0");
356 let message = control_data.substring(0,off);
357 exit_status = parseInt(message);
359 /* wait for all fd sockets to close? */
361 wait_for_fd_sockets();
367 input_stream_async_wait(control_input_stream, handle_control_input);
369 // Control socket closed: terminate
375 var registered_fds = 0;
379 onSocketAccepted: function (server, transport) {
380 unregistered_transports.push(transport);
381 function remove_from_unregistered () {
383 i = unregistered_transports.indexOf(transport);
385 unregistered_transports.splice(i, 1);
392 remove_from_unregistered();
394 var received_data = "";
395 var header_size = key_length + fd_spec_size;
397 var in_stream, bin_stream, out_stream;
399 function handle_input () {
400 if (terminate_pending)
403 let remaining = header_size - received_data.length;
404 let avail = in_stream.available();
406 if (avail > remaining)
408 received_data += bin_stream.readBytes(avail);
410 if (received_data.length < header_size) {
411 input_stream_async_wait(in_stream, handle_input);
414 if (received_data.substring(0, key_length) != client_key)
421 var fdspec = received_data.substring(key_length);
422 if (fdspec.charCodeAt(0) == 0) {
424 // This is the control connection
425 if (control_transport)
426 throw "Control transport already exists";
427 control_transport = transport;
428 control_output_stream = out_stream;
429 control_input_stream = in_stream;
430 control_binary_input_stream = bin_stream;
431 remove_from_unregistered();
433 var fd = parseInt(fdspec);
434 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
436 remove_from_unregistered();
438 registered_transports[fd] = {transport: transport,
443 if (control_transport && registered_fds == total_fds) {
445 control_state = CONTROL_SENDING_KEY;
446 async_binary_write(control_output_stream, server_key,
450 control_state = CONTROL_SENT_KEY;
452 setup_timer.cancel();
455 if (terminate_pending) {
456 control_send_terminate();
460 let t = registered_transports[i];
472 input_stream_async_wait(control_input_stream, handle_control_input);
480 in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
481 out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
482 bin_stream = binary_input_stream(in_stream);
483 input_stream_async_wait(in_stream, handle_input);
488 onStopListening: function (s, status) {
492 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
493 return make_cancelable(deferred.promise, canceler);
497 if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
499 throw new Error("Error spawning process: not yet supported on MS Windows");
501 throw new Error("Error spawning process: conkeror-spawn-helper not found");
503 // Allow the exception to propagate to the caller
509 * spawn_process_blind: spawn a process and forget about it
511 define_keywords("$cwd", "$fds");
512 function spawn_process_blind (program_name, args) {
514 /* Check if we can use spawn_process_internal */
515 var cwd = arguments.$cwd;
516 var fds = arguments.$fds;
517 if (cwd == null && fds == null && args[0] == null)
518 spawn_process_internal(program_name, args.slice(1));
520 spawn_process(program_name, args, cwd, fds);
525 // Keyword arguments: $cwd, $fds
526 function spawn_and_wait_for_process (program_name, args) {
527 keywords(arguments, $cwd = null, $fds = null);
528 let result = yield spawn_process(program_name, args, arguments.$cwd,
530 yield co_return(result);
533 // Keyword arguments: $cwd, $fds
534 function shell_command_blind (cmd) {
536 /* Check if we can use spawn_process_internal */
537 var cwd = arguments.$cwd;
538 var fds = arguments.$fds;
546 full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
549 program_name = getenv("SHELL") || "/bin/sh";
550 args = [null, "-c", full_cmd];
555 if (cwd.path.match(/[a-z]:/i)) {
556 full_cmd += cwd.path.substring(0,2) + " && ";
558 full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
562 /* Need to convert the single command-line into a list of
563 * arguments that will then get converted back into a *
564 command-line by Mozilla. */
565 var out = [null, "/C"];
568 for (var i = 0; i < full_cmd.length; ++i) {
569 var ch = full_cmd[i];
585 if (cur_arg.length > 0)
587 program_name = "cmd.exe";
590 spawn_process_blind(program_name, args, $fds = arguments.$fds);
593 function substitute_shell_command_argument (cmdline, argument) {
594 if (!cmdline.match("{}"))
595 return cmdline + " \"" + shell_quote(argument) + "\"";
597 return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
600 function shell_command_with_argument_blind (command, arg) {
601 shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
606 * $cwd: The current working directory for the process.
607 * $fds: File descriptors to use.
609 function shell_command (command) {
611 throw new Error("shell_command: Your OS is not yet supported");
612 var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
613 [null, "-c", command],
614 forward_keywords(arguments));
615 yield co_return(result);
618 function shell_command_with_argument (command, arg) {
619 yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));
622 provide("spawn-process");