new module to enable editing and deleting of bookmarks
[conkeror/arlinius.git] / modules / spawn-process.js
blob5e94175f2cd7a059a95dfdb8ca88e27569d460e3
1 /**
2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4 * Use, modification, and distribution are subject to the terms specified in the
5 * COPYING file.
6 **/
8 require("interactive.js");
9 require("io.js");
10 require("env.js");
12 function spawn_process_internal (program, args, blocking) {
13 var process = Cc["@mozilla.org/process/util;1"]
14 .createInstance(Ci.nsIProcess);
15 process.init(find_file_in_path(program));
16 return process.run(!!blocking, args, args.length);
19 var PATH_programs = null;
20 function get_shell_command_completer () {
21 if (PATH_programs == null) {
22 PATH_programs = [];
23 var file = Cc["@mozilla.org/file/local;1"]
24 .createInstance(Ci.nsILocalFile);
25 for (var i = 0, plen = PATH.length; i < plen; ++i) {
26 try {
27 file.initWithPath(PATH[i]);
28 var entries = file.directoryEntries;
29 while (entries.hasMoreElements()) {
30 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
31 PATH_programs.push(entry.leafName);
33 } catch (e) {}
35 PATH_programs.sort();
37 return prefix_completer($completions = PATH_programs,
38 $get_string = function (x) x);
41 // use default
42 minibuffer_auto_complete_preferences["shell-command"] = null;
44 /* FIXME: support a relative or full path as well as PATH commands */
45 define_keywords("$cwd");
46 minibuffer.prototype.read_shell_command = function () {
47 keywords(arguments, $history = "shell-command");
48 var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd.path + "]:";
49 var result = yield this.read(
50 $prompt = prompt,
51 $history = "shell-command",
52 $auto_complete = "shell-command",
53 $select,
54 $validator = function (x, m) {
55 var s = x.replace(/^\s+|\s+$/g, '');
56 if (s.length == 0) {
57 m.message("A blank shell command is not allowed.");
58 return false;
60 return true;
62 forward_keywords(arguments),
63 $completer = get_shell_command_completer());
64 yield co_return(result);
67 function find_spawn_helper () {
68 var f = file_locator_service.get("CurProcD", Ci.nsIFile);
69 f.append("conkeror-spawn-helper");
70 if (f.exists())
71 return f;
72 return find_file_in_path("conkeror-spawn-helper");
75 const STDIN_FILENO = 0;
76 const STDOUT_FILENO = 1;
77 const STDERR_FILENO = 2;
79 var spawn_process_helper_default_fd_wait_timeout = 1000;
80 var spawn_process_helper_setup_timeout = 2000;
82 /**
83 * @param program_name
84 * Specifies the full path to the program.
85 * @param args
86 * An array of strings to pass as the arguments to the program. The
87 * first argument should be the program name. These strings must not
88 * have any NUL bytes in them.
89 * @param working_dir
90 * If non-null, must be an nsILocalFile. spawn_process will switch
91 * to this path before running the program.
92 * @param finished_callback
93 * Called with a single argument, the exit code of the process, as
94 * returned by the wait system call.
95 * @param failure_callback
96 * Called with a single argument, an exception, if one occurs.
97 * @param fds
98 * If non-null, must be an object with only non-negative integer
99 * properties set. Each such property specifies that the corresponding
100 * file descriptor in the spawned process should be redirected. Note
101 * that 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2
102 * corresponds to STDERR. Note that every redirected file descriptor can
103 * be used for both input and output, although STDIN, STDOUT, and STDERR
104 * are typically used only unidirectionally. Each property must be an
105 * object itself, with an input and/or output property specifying
106 * callback functions that are called with an nsIAsyncInputStream or
107 * nsIAsyncOutputStream when the stream for that file descriptor is
108 * available.
109 * @param fd_wait_timeout
110 * Specifies the number of milliseconds to wait for the file descriptor
111 * redirection sockets to be closed after the control socket indicates
112 * the process has exited before they are closed forcefully. A negative
113 * value means to wait indefinitely. If fd_wait_timeout is null,
114 * spawn_process_helper_default_fd_wait_timeout is used instead.
115 * @return
116 * A function that can be called to prematurely terminate the spawned
117 * process.
119 function spawn_process (program_name, args, working_dir,
120 success_callback, failure_callback, fds,
121 fd_wait_timeout) {
122 var spawn_process_helper_program = find_spawn_helper();
123 if (spawn_process_helper_program == null)
124 throw new Error("Error spawning process: conkeror-spawn-helper not found");
125 args = args.slice();
126 if (args[0] == null)
127 args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name;
129 program_name = find_file_in_path(program_name).path;
131 const key_length = 100;
132 const fd_spec_size = 15;
134 if (fds == null)
135 fds = {};
137 if (fd_wait_timeout === undefined)
138 fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout;
140 var unregistered_transports = [];
141 var registered_transports = [];
143 var server = null;
144 var setup_timer = null;
146 const CONTROL_CONNECTED = 0;
147 const CONTROL_SENDING_KEY = 1;
148 const CONTROL_SENT_KEY = 2;
150 var control_state = CONTROL_CONNECTED;
151 var terminate_pending = false;
153 var control_transport = null;
155 var control_binary_input_stream = null;
156 var control_output_stream = null, control_input_stream = null;
157 var exit_status = null;
159 var client_key = "";
160 var server_key = "";
161 // Make sure key does not have any 0 bytes in it.
162 for (let i = 0; i < key_length; ++i)
163 client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
165 // Make sure key does not have any 0 bytes in it.
166 for (let i = 0; i < key_length; ++i)
167 server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1);
169 var key_file_fd_data = "";
171 // This is the total number of redirected file descriptors.
172 var total_client_fds = 0;
174 // This is the total number of redirected file descriptors that will use a socket connection.
175 var total_fds = 0;
177 for (let i in fds) {
178 if (fds.hasOwnProperty(i)) {
179 if (fds[i] == null) {
180 delete fds[i];
181 continue;
183 key_file_fd_data += i + "\0";
184 let fd = fds[i];
185 if ('file' in fd) {
186 if (fd.perms == null)
187 fd.perms = 0666;
188 key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0";
189 delete fds[i]; // Remove it from fds, as we won't need to work with it anymore
190 } else {
191 ++total_fds;
192 key_file_fd_data += "\0";
194 ++total_client_fds;
197 var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" +
198 (working_dir != null ? working_dir.path : "") + "\0" +
199 args.length + "\0" +
200 args.join("\0") + "\0" +
201 total_client_fds + "\0" + key_file_fd_data;
203 function fail (e) {
204 if (!terminate_pending) {
205 terminate();
206 if (failure_callback)
207 failure_callback(e);
211 function cleanup_server () {
212 if (server) {
213 server.close();
214 server = null;
216 for (let i in unregistered_transports) {
217 unregistered_transports[i].close(0);
218 delete unregistered_transports[i];
222 function cleanup_fd_sockets () {
223 for (let i in registered_transports) {
224 registered_transports[i].transport.close(0);
225 delete registered_transports[i];
229 function cleanup_control () {
230 if (control_transport) {
231 control_binary_input_stream.close();
232 control_binary_input_stream = null;
233 control_transport.close(0);
234 control_transport = null;
235 control_input_stream = null;
236 control_output_stream = null;
240 function control_send_terminate () {
241 control_input_stream = null;
242 control_binary_input_stream.close();
243 control_binary_input_stream = null;
244 async_binary_write(control_output_stream, "\0", function () {
245 control_output_stream = null;
246 control_transport.close(0);
247 control_transport = null;
251 function terminate () {
252 if (terminate_pending)
253 return exit_status;
254 terminate_pending = true;
255 if (setup_timer) {
256 setup_timer.cancel();
257 setup_timer = null;
259 cleanup_server();
260 cleanup_fd_sockets();
261 if (control_transport) {
262 switch (control_state) {
263 case CONTROL_SENT_KEY:
264 control_send_terminate();
265 break;
266 case CONTROL_CONNECTED:
267 cleanup_control();
268 break;
270 * case CONTROL_SENDING_KEY: in this case once the key
271 * is sent, the terminate_pending flag will be noticed
272 * and control_send_terminate will be called, so nothing
273 * more needs to be done here.
277 return exit_status;
280 function finished () {
281 // Only call success_callback if terminate was not already called
282 if (!terminate_pending) {
283 terminate();
284 if (success_callback)
285 success_callback(exit_status);
289 // Create server socket to listen for connections from the external helper program
290 try {
291 server = Cc['@mozilla.org/network/server-socket;1']
292 .createInstance(Ci.nsIServerSocket);
294 var key_file = get_temporary_file("conkeror-spawn-helper-key.dat");
296 write_binary_file(key_file, key_file_data);
297 server.init(-1 /* choose a port automatically */,
298 true /* bind to localhost only */,
299 -1 /* select backlog size automatically */);
301 setup_timer = call_after_timeout(function () {
302 setup_timer = null;
303 if (control_state != CONTROL_SENT_KEY)
304 fail("setup timeout");
305 }, spawn_process_helper_setup_timeout);
307 function wait_for_fd_sockets () {
308 var remaining_streams = total_fds * 2;
309 var timer = null;
310 function handler () {
311 if (remaining_streams != null) {
312 --remaining_streams;
313 if (remaining_streams == 0) {
314 if (timer)
315 timer.cancel();
316 finished();
320 for each (let f in registered_transports) {
321 input_stream_async_wait(f.input, handler, false /* wait for closure */);
322 output_stream_async_wait(f.output, handler, false /* wait for closure */);
324 if (fd_wait_timeout != null) {
325 timer = call_after_timeout(function() {
326 remaining_streams = null;
327 finished();
328 }, fd_wait_timeout);
332 var control_data = "";
334 function handle_control_input () {
335 if (terminate_pending)
336 return;
337 try {
338 let avail = control_input_stream.available();
339 if (avail > 0) {
340 control_data += control_binary_input_stream.readBytes(avail);
341 var off = control_data.indexOf("\0");
342 if (off >= 0) {
343 let message = control_data.substring(0,off);
344 exit_status = parseInt(message);
345 cleanup_control();
346 /* wait for all fd sockets to close? */
347 if (total_fds > 0)
348 wait_for_fd_sockets();
349 else
350 finished();
351 return;
354 input_stream_async_wait(control_input_stream, handle_control_input);
355 } catch (e) {
356 // Control socket closed: terminate
357 cleanup_control();
358 fail(e);
362 var registered_fds = 0;
364 server.asyncListen(
366 onSocketAccepted: function (server, transport) {
367 unregistered_transports.push(transport);
368 function remove_from_unregistered () {
369 var i;
370 i = unregistered_transports.indexOf(transport);
371 if (i >= 0) {
372 unregistered_transports.splice(i, 1);
373 return true;
375 return false;
377 function close () {
378 transport.close(0);
379 remove_from_unregistered();
381 var received_data = "";
382 var header_size = key_length + fd_spec_size;
384 var in_stream, bin_stream, out_stream;
386 function handle_input () {
387 if (terminate_pending)
388 return;
389 try {
390 let remaining = header_size - received_data.length;
391 let avail = in_stream.available();
392 if (avail > 0) {
393 if (avail > remaining)
394 avail = remaining;
395 received_data += bin_stream.readBytes(avail);
397 if (received_data.length < header_size) {
398 input_stream_async_wait(in_stream, handle_input);
399 return;
400 } else {
401 if (received_data.substring(0, key_length) != client_key)
402 throw "Invalid key";
404 } catch (e) {
405 close();
407 try {
408 var fdspec = received_data.substring(key_length);
409 if (fdspec.charCodeAt(0) == 0) {
411 // This is the control connection
412 if (control_transport)
413 throw "Control transport already exists";
414 control_transport = transport;
415 control_output_stream = out_stream;
416 control_input_stream = in_stream;
417 control_binary_input_stream = bin_stream;
418 remove_from_unregistered();
419 } else {
420 var fd = parseInt(fdspec);
421 if (!fds.hasOwnProperty(fd) || (fd in registered_transports))
422 throw "Invalid fd";
423 remove_from_unregistered();
424 bin_stream = null;
425 registered_transports[fd] = {transport: transport,
426 input: in_stream,
427 output: out_stream};
428 ++registered_fds;
430 if (control_transport && registered_fds == total_fds) {
431 cleanup_server();
432 control_state = CONTROL_SENDING_KEY;
433 async_binary_write(control_output_stream, server_key,
434 function (error) {
435 if (error != null)
436 fail(error);
437 control_state = CONTROL_SENT_KEY;
438 if (setup_timer) {
439 setup_timer.cancel();
440 setup_timer = null;
442 if (terminate_pending) {
443 control_send_terminate();
444 } else {
445 for (let i in fds) {
446 let f = fds[i];
447 let t = registered_transports[i];
448 if ('input' in f)
449 f.input(t.input);
450 else
451 t.input.close();
452 if ('output' in f)
453 f.output(t.output);
454 else
455 t.output.close();
459 input_stream_async_wait(control_input_stream, handle_control_input);
461 } catch (e) {
462 fail(e);
466 try {
467 in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
468 out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0);
469 bin_stream = binary_input_stream(in_stream);
470 input_stream_async_wait(in_stream, handle_input);
471 } catch (e) {
472 close();
475 onStopListening: function (s, status) {
479 spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false);
480 return terminate;
481 } catch (e) {
482 terminate();
484 if ((e instanceof Ci.nsIException) && e.result == Cr.NS_ERROR_INVALID_POINTER) {
485 if (WINDOWS)
486 throw new Error("Error spawning process: not yet supported on MS Windows");
487 else
488 throw new Error("Error spawning process: conkeror-spawn-helper not found");
490 // Allow the exception to propagate to the caller
491 throw e;
496 * spawn_process_blind: spawn a process and forget about it
498 define_keywords("$cwd", "$fds");
499 function spawn_process_blind (program_name, args) {
500 keywords(arguments);
501 /* Check if we can use spawn_process_internal */
502 var cwd = arguments.$cwd;
503 var fds = arguments.$fds;
504 if (cwd == null && fds == null && args[0] == null)
505 spawn_process_internal(program_name, args.slice(1));
506 else {
507 spawn_process(program_name, args, cwd,
508 null /* success callback */,
509 null /* failure callback */,
510 fds);
515 // Keyword arguments: $cwd, $fds
516 function spawn_and_wait_for_process (program_name, args) {
517 keywords(arguments, $cwd = null, $fds = null);
518 var cc = yield CONTINUATION;
519 spawn_process(program_name, args, arguments.$cwd,
520 cc, cc.throw,
521 arguments.$fds);
522 var result = yield SUSPEND;
523 yield co_return(result);
526 // Keyword arguments: $cwd, $fds
527 function shell_command_blind (cmd) {
528 keywords(arguments);
529 /* Check if we can use spawn_process_internal */
530 var cwd = arguments.$cwd;
531 var fds = arguments.$fds;
533 var program_name;
534 var args;
536 if (POSIX) {
537 var full_cmd;
538 if (cwd)
539 full_cmd = "cd \"" + shell_quote(cwd.path) + "\"; " + cmd;
540 else
541 full_cmd = cmd;
542 program_name = getenv("SHELL") || "/bin/sh";
543 args = [null, "-c", full_cmd];
544 } else {
545 var full_cmd;
546 if (cwd) {
547 full_cmd = "";
548 if (cwd.path.match(/[a-z]:/i)) {
549 full_cmd += cwd.path.substring(0,2) + " && ";
551 full_cmd += "cd \"" + shell_quote(cwd.path) + "\" && " + cmd;
552 } else
553 full_cmd = cmd;
555 /* Need to convert the single command-line into a list of
556 * arguments that will then get converted back into a *
557 command-line by Mozilla. */
558 var out = [null, "/C"];
559 var cur_arg = "";
560 var quoting = false;
561 for (var i = 0; i < full_cmd.length; ++i) {
562 var ch = full_cmd[i];
563 if (ch == " ") {
564 if (quoting) {
565 cur_arg += ch;
566 } else {
567 out.push(cur_arg);
568 cur_arg = "";
570 continue;
572 if (ch == "\"") {
573 quoting = !quoting;
574 continue;
576 cur_arg += ch;
578 if (cur_arg.length > 0)
579 out.push(cur_arg);
580 program_name = "cmd.exe";
581 args = out;
583 spawn_process_blind(program_name, args, $fds = arguments.$fds);
586 function substitute_shell_command_argument (cmdline, argument) {
587 if (!cmdline.match("{}"))
588 return cmdline + " \"" + shell_quote(argument) + "\"";
589 else
590 return cmdline.replace("{}", "\"" + shell_quote(argument) + "\"");
593 function shell_command_with_argument_blind (command, arg) {
594 shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments));
598 * Keyword arguments:
599 * $cwd: The current working directory for the process.
600 * $fds: File descriptors to use.
602 function shell_command (command) {
603 if (!POSIX)
604 throw new Error("shell_command: Your OS is not yet supported");
605 var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh",
606 [null, "-c", command],
607 forward_keywords(arguments));
608 yield co_return(result);
611 function shell_command_with_argument (command, arg) {
612 yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments))));
615 provide("spawn-process");