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.
99 * If non-null, must be an object with only non-negative integer
100 * properties set. Each such property specifies that the corresponding
101 * file descriptor in the spawned process should be redirected. Note
102 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
103 * corresponds to STDERR. Note that every redirected file descriptor can
104 * be used for both input and output, although STDIN, STDOUT, and STDERR
105 * are typically used only unidirectionally. Each property must be an
106 * object itself, with an input and/or output property specifying
107 * callback functions that are called with an nsIAsyncInputStream or
108 * nsIAsyncOutputStream when the stream for that file descriptor is
110 * @param fd_wait_timeout
111 * Specifies the number of milliseconds to wait for the file descriptor
112 * redirection sockets to be closed after the control socket indicates
113 * the process has exited before they are closed forcefully. A negative
114 * value means to wait indefinitely. If fd_wait_timeout is null,
115 * spawn_process_helper_default_fd_wait_timeout is used instead.
117 * A function that can be called to prematurely terminate the spawned
120 function spawn_process (program_name
, args
, working_dir
,
121 fds
, fd_wait_timeout
) {
123 let deferred
= Promise
.defer();
125 var spawn_process_helper_program
= find_spawn_helper();
126 if (spawn_process_helper_program
== null)
127 throw new Error("Error spawning process: conkeror-spawn-helper not found");
130 args
[0] = (program_name
instanceof Ci
.nsIFile
) ? program_name
.path
: program_name
;
132 program_name
= find_file_in_path(program_name
).path
;
134 const key_length
= 100;
135 const fd_spec_size
= 15;
140 if (fd_wait_timeout
=== undefined)
141 fd_wait_timeout
= spawn_process_helper_default_fd_wait_timeout
;
143 var unregistered_transports
= [];
144 var registered_transports
= [];
147 var setup_timer
= null;
149 const CONTROL_CONNECTED
= 0;
150 const CONTROL_SENDING_KEY
= 1;
151 const CONTROL_SENT_KEY
= 2;
153 var control_state
= CONTROL_CONNECTED
;
154 var terminate_pending
= false;
156 var control_transport
= null;
158 var control_binary_input_stream
= null;
159 var control_output_stream
= null, control_input_stream
= null;
160 var exit_status
= null;
164 // Make sure key does not have any 0 bytes in it.
165 for (let i
= 0; i
< key_length
; ++i
)
166 client_key
+= String
.fromCharCode(Math
.floor(Math
.random() * 255) + 1);
168 // Make sure key does not have any 0 bytes in it.
169 for (let i
= 0; i
< key_length
; ++i
)
170 server_key
+= String
.fromCharCode(Math
.floor(Math
.random() * 255) + 1);
172 var key_file_fd_data
= "";
174 // This is the total number of redirected file descriptors.
175 var total_client_fds
= 0;
177 // This is the total number of redirected file descriptors that will use a socket connection.
181 if (fds
.hasOwnProperty(i
)) {
182 if (fds
[i
] == null) {
186 key_file_fd_data
+= i
+ "\0";
189 if (fd
.perms
== null)
190 fd
.perms
= parseInt("0666", 8);
191 key_file_fd_data
+= fd
.file
+ "\0" + fd
.mode
+ "\0" + fd
.perms
+ "\0";
192 delete fds
[i
]; // Remove it from fds, as we won't need to work with it anymore
195 key_file_fd_data
+= "\0";
200 var key_file_data
= client_key
+ "\0" + server_key
+ "\0" + program_name
+ "\0" +
201 (working_dir
!= null ? working_dir
.path
: "") + "\0" +
203 args
.join("\0") + "\0" +
204 total_client_fds
+ "\0" + key_file_fd_data
;
207 if (!terminate_pending
) {
213 function cleanup_server () {
218 for (let i
in unregistered_transports
) {
219 unregistered_transports
[i
].close(0);
220 delete unregistered_transports
[i
];
224 function cleanup_fd_sockets () {
225 for (let i
in registered_transports
) {
226 registered_transports
[i
].transport
.close(0);
227 delete registered_transports
[i
];
231 function cleanup_control () {
232 if (control_transport
) {
233 control_binary_input_stream
.close();
234 control_binary_input_stream
= null;
235 control_transport
.close(0);
236 control_transport
= null;
237 control_input_stream
= null;
238 control_output_stream
= null;
242 function control_send_terminate () {
243 control_input_stream
= null;
244 control_binary_input_stream
.close();
245 control_binary_input_stream
= null;
246 async_binary_write(control_output_stream
, "\0", function () {
247 control_output_stream
= null;
248 control_transport
.close(0);
249 control_transport
= null;
253 function terminate () {
254 if (terminate_pending
)
256 terminate_pending
= true;
258 setup_timer
.cancel();
262 cleanup_fd_sockets();
263 if (control_transport
) {
264 switch (control_state
) {
265 case CONTROL_SENT_KEY
:
266 control_send_terminate();
268 case CONTROL_CONNECTED
:
272 * case CONTROL_SENDING_KEY: in this case once the key
273 * is sent, the terminate_pending flag will be noticed
274 * and control_send_terminate will be called, so nothing
275 * more needs to be done here.
282 function canceler (e
) {
283 if (!terminate_pending
) {
289 function finished () {
290 if (!terminate_pending
) {
291 deferred
.resolve(exit_status
);
296 // Create server socket to listen for connections from the external helper program
298 server
= Cc
['@mozilla.org/network/server-socket;1']
299 .createInstance(Ci
.nsIServerSocket
);
301 var key_file
= get_temporary_file("conkeror-spawn-helper-key.dat");
303 write_binary_file(key_file
, key_file_data
);
304 server
.init(-1 /* choose a port automatically */,
305 true /* bind to localhost only */,
306 -1 /* select backlog size automatically */);
308 setup_timer
= call_after_timeout(function () {
310 if (control_state
!= CONTROL_SENT_KEY
)
311 fail("setup timeout");
312 }, spawn_process_helper_setup_timeout
);
314 var wait_for_fd_sockets
= function wait_for_fd_sockets () {
315 var remaining_streams
= total_fds
* 2;
317 function handler () {
318 if (remaining_streams
!= null) {
320 if (remaining_streams
== 0) {
327 for each (let f
in registered_transports
) {
328 input_stream_async_wait(f
.input
, handler
, false /* wait for closure */);
329 output_stream_async_wait(f
.output
, handler
, false /* wait for closure */);
331 if (fd_wait_timeout
!= null) {
332 timer
= call_after_timeout(function() {
333 remaining_streams
= null;
339 var control_data
= "";
341 var handle_control_input
= function handle_control_input () {
342 if (terminate_pending
)
345 let avail
= control_input_stream
.available();
347 control_data
+= control_binary_input_stream
.readBytes(avail
);
348 var off
= control_data
.indexOf("\0");
350 let message
= control_data
.substring(0,off
);
351 exit_status
= parseInt(message
);
353 /* wait for all fd sockets to close? */
355 wait_for_fd_sockets();
361 input_stream_async_wait(control_input_stream
, handle_control_input
);
363 // Control socket closed: terminate
369 var registered_fds
= 0;
373 onSocketAccepted: function (server
, transport
) {
374 unregistered_transports
.push(transport
);
375 function remove_from_unregistered () {
377 i
= unregistered_transports
.indexOf(transport
);
379 unregistered_transports
.splice(i
, 1);
386 remove_from_unregistered();
388 var received_data
= "";
389 var header_size
= key_length
+ fd_spec_size
;
391 var in_stream
, bin_stream
, out_stream
;
393 function handle_input () {
394 if (terminate_pending
)
397 let remaining
= header_size
- received_data
.length
;
398 let avail
= in_stream
.available();
400 if (avail
> remaining
)
402 received_data
+= bin_stream
.readBytes(avail
);
404 if (received_data
.length
< header_size
) {
405 input_stream_async_wait(in_stream
, handle_input
);
408 if (received_data
.substring(0, key_length
) != client_key
)
415 var fdspec
= received_data
.substring(key_length
);
416 if (fdspec
.charCodeAt(0) == 0) {
418 // This is the control connection
419 if (control_transport
)
420 throw "Control transport already exists";
421 control_transport
= transport
;
422 control_output_stream
= out_stream
;
423 control_input_stream
= in_stream
;
424 control_binary_input_stream
= bin_stream
;
425 remove_from_unregistered();
427 var fd
= parseInt(fdspec
);
428 if (!fds
.hasOwnProperty(fd
) || (fd
in registered_transports
))
430 remove_from_unregistered();
432 registered_transports
[fd
] = {transport
: transport
,
437 if (control_transport
&& registered_fds
== total_fds
) {
439 control_state
= CONTROL_SENDING_KEY
;
440 async_binary_write(control_output_stream
, server_key
,
444 control_state
= CONTROL_SENT_KEY
;
446 setup_timer
.cancel();
449 if (terminate_pending
) {
450 control_send_terminate();
454 let t
= registered_transports
[i
];
466 input_stream_async_wait(control_input_stream
, handle_control_input
);
474 in_stream
= transport
.openInputStream(Ci
.nsITransport
.OPEN_NON_BLOCKING
, 0, 0);
475 out_stream
= transport
.openOutputStream(Ci
.nsITransport
.OPEN_NON_BLOCKING
, 0, 0);
476 bin_stream
= binary_input_stream(in_stream
);
477 input_stream_async_wait(in_stream
, handle_input
);
482 onStopListening: function (s
, status
) {
486 spawn_process_internal(spawn_process_helper_program
, [key_file
.path
, server
.port
], false);
487 return make_cancelable(deferred
.promise
, canceler
);
491 if ((e
instanceof Ci
.nsIException
) && e
.result
== Cr
.NS_ERROR_INVALID_POINTER
) {
493 throw new Error("Error spawning process: not yet supported on MS Windows");
495 throw new Error("Error spawning process: conkeror-spawn-helper not found");
497 // Allow the exception to propagate to the caller
503 * spawn_process_blind: spawn a process and forget about it
505 define_keywords("$cwd", "$fds");
506 function spawn_process_blind (program_name
, args
) {
508 /* Check if we can use spawn_process_internal */
509 var cwd
= arguments
.$cwd
;
510 var fds
= arguments
.$fds
;
511 if (cwd
== null && fds
== null && args
[0] == null)
512 spawn_process_internal(program_name
, args
.slice(1));
514 spawn_process(program_name
, args
, cwd
, fds
);
519 // Keyword arguments: $cwd, $fds
520 function spawn_and_wait_for_process (program_name
, args
) {
521 keywords(arguments
, $cwd
= null, $fds
= null);
522 let result
= yield spawn_process(program_name
, args
, arguments
.$cwd
,
524 yield co_return(result
);
527 // Keyword arguments: $cwd, $fds
528 function shell_command_blind (cmd
) {
530 /* Check if we can use spawn_process_internal */
531 var cwd
= arguments
.$cwd
;
532 var fds
= arguments
.$fds
;
540 full_cmd
= "cd \"" + shell_quote(cwd
.path
) + "\"; " + cmd
;
543 program_name
= getenv("SHELL") || "/bin/sh";
544 args
= [null, "-c", full_cmd
];
549 if (cwd
.path
.match(/[a-z]:/i)) {
550 full_cmd
+= cwd
.path
.substring(0,2) + " && ";
552 full_cmd
+= "cd \"" + shell_quote(cwd
.path
) + "\" && " + cmd
;
556 /* Need to convert the single command-line into a list of
557 * arguments that will then get converted back into a *
558 command-line by Mozilla. */
559 var out
= [null, "/C"];
562 for (var i
= 0; i
< full_cmd
.length
; ++i
) {
563 var ch
= full_cmd
[i
];
579 if (cur_arg
.length
> 0)
581 program_name
= "cmd.exe";
584 spawn_process_blind(program_name
, args
, $fds
= arguments
.$fds
);
587 function substitute_shell_command_argument (cmdline
, argument
) {
588 if (!cmdline
.match("{}"))
589 return cmdline
+ " \"" + shell_quote(argument
) + "\"";
591 return cmdline
.replace("{}", "\"" + shell_quote(argument
) + "\"");
594 function shell_command_with_argument_blind (command
, arg
) {
595 shell_command_blind(substitute_shell_command_argument(command
, arg
), forward_keywords(arguments
));
600 * $cwd: The current working directory for the process.
601 * $fds: File descriptors to use.
603 function shell_command (command
) {
605 throw new Error("shell_command: Your OS is not yet supported");
606 var result
= yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
607 [null, "-c", command
],
608 forward_keywords(arguments
));
609 yield co_return(result
);
612 function shell_command_with_argument (command
, arg
) {
613 yield co_return((yield shell_command(substitute_shell_command_argument(command
, arg
), forward_keywords(arguments
))));
616 provide("spawn-process");