From 9fd3ae61e740b13546d6382fd296c3fe504a164e Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 1 Apr 2008 10:16:17 -0400 Subject: [PATCH] Implemented new, more powerful framework for spawning proceses This new framework allows starting processes in a specific working directory, allows arbitrary file descriptor redirections, and does not depend on multi-threading for notification of when the process exits. Instead, it uses an external helper program which it talks to over a TCP socket. In order to run external programs, it is necessary to build the external helper program by running: make. Currently the new framework does not work with MS Windows. --- .gitignore | 1 + Makefile | 4 + modules/buffer.js | 4 +- modules/conkeror.js | 5 +- modules/download-manager.js | 8 +- modules/element.js | 2 +- modules/external-editor.js | 6 +- modules/io.js | 193 +++++++++++++ modules/localfile.js | 270 ------------------ modules/minibuffer-read-file.js | 69 +++++ modules/process.js | 155 ----------- modules/spawn-process.js | 593 ++++++++++++++++++++++++++++++++++++++++ modules/thread.js | 61 ----- modules/utils.js | 2 +- spawn-process-helper.c | 345 +++++++++++++++++++++++ 15 files changed, 1218 insertions(+), 500 deletions(-) create mode 100644 Makefile create mode 100644 modules/io.js delete mode 100644 modules/localfile.js create mode 100644 modules/minibuffer-read-file.js delete mode 100644 modules/process.js create mode 100644 modules/spawn-process.js delete mode 100644 modules/thread.js create mode 100644 spawn-process-helper.c diff --git a/.gitignore b/.gitignore index 559d8bb..d9f524c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ #*# .#* xulrunner-stub +spawn-process-helper diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..357ef82 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ + +CFLAGS = -O2 -g + +spawn-process-helper: spawn-process-helper.c diff --git a/modules/buffer.js b/modules/buffer.js index d6bfd07..79204f1 100644 --- a/modules/buffer.js +++ b/modules/buffer.js @@ -593,8 +593,8 @@ interactive("change-current-directory", interactive("shell-command", function (I) { var cwd = I.cwd; - var cmd = I.minibuffer.read_shell_command($cwd = cwd); - shell_command(cwd, cmd); + var cmd = (yield I.minibuffer.read_shell_command($cwd = cwd)); + yield shell_command(cmd, $cwd = cwd); }); function unfocus(buffer) diff --git a/modules/conkeror.js b/modules/conkeror.js index 66f83cb..5cbcc81 100644 --- a/modules/conkeror.js +++ b/modules/conkeror.js @@ -12,10 +12,9 @@ require("minibuffer.js"); // depends: interactive.js require("minibuffer-read.js"); require("minibuffer-read-option.js"); require("minibuffer-completion.js"); -require("localfile.js"); -require("thread.js"); +require("minibuffer-read-file.js"); require("timer.js"); -require("process.js"); +require("spawn-process.js"); require("mime.js"); require("keyboard.js"); require("buffer.js"); diff --git a/modules/download-manager.js b/modules/download-manager.js index b879f3d..81a4927 100644 --- a/modules/download-manager.js +++ b/modules/download-manager.js @@ -382,9 +382,9 @@ var download_progress_listener = { info.running_shell_command = true; co_call(function () { try { - yield shell_command_with_argument(info.shell_command_cwd, - info.shell_command, - info.target_file.path); + yield shell_command_with_argument(info.shell_command, + info.target_file.path, + $cwd = info.shell_command_cwd); } finally { if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND) info.target_file.remove(false /* not recursive */); @@ -900,7 +900,7 @@ function download_shell_command(buffer, cwd, cmd) { check_buffer(buffer, download_buffer); var info = buffer.info; if (info.state == DOWNLOAD_FINISHED) { - shell_command_with_argument_sync(cwd, cmd, info.target_file.path, false); + shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd); return; } if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED) diff --git a/modules/element.js b/modules/element.js index fac65b2..a153bce 100644 --- a/modules/element.js +++ b/modules/element.js @@ -509,7 +509,7 @@ interactive("shell-command-on-url", function (I) { panel.destroy(); } - shell_command_with_argument_sync(cwd, cmd, uri, false); + shell_command_with_argument_blind(cmd, uri, $cwd = cwd); }); function browser_element_shell_command(buffer, elem, command) { diff --git a/modules/external-editor.js b/modules/external-editor.js index db61b1c..d98616c 100644 --- a/modules/external-editor.js +++ b/modules/external-editor.js @@ -30,7 +30,7 @@ function open_file_with_external_editor(file) { cmd += "\"" + shell_quote(file.path) + "\""; try { - yield shell_command(default_directory.path, cmd); + yield shell_command(cmd); } finally { if (arguments.$temporary) { try { @@ -43,12 +43,12 @@ function open_file_with_external_editor(file) { function create_external_editor_launcher(program, args) { return function (file) { keywords(arguments); - var arr = args.slice(); + var arr = [null].concat(args.slice()); if (arguments.$line != null) arr.push("+" + arguments.$line); arr.push(file.path); try { - yield spawn_process(program, arr); + yield spawn_and_wait_for_process(program, arr); } finally { if (arguments.$temporary) { try { diff --git a/modules/io.js b/modules/io.js new file mode 100644 index 0000000..2d8df33 --- /dev/null +++ b/modules/io.js @@ -0,0 +1,193 @@ +const PERM_IWOTH = 00002; /* write permission, others */ +const PERM_IWGRP = 00020; /* write permission, group */ + +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_RDWR = 0x04; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; +const MODE_SYNC = 0x40; +const MODE_EXCL = 0x80; +const MODE_OUTPUT = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; +const MODE_OUTPUT_APPEND = MODE_WRONLY | MODE_CREATE | MODE_APPEND; +const MODE_INPUT = MODE_RDONLY; + +define_keywords("$charset"); +function read_text_file(file) +{ + keywords(arguments, $charset = "UTF-8"); + + var ifstream = null, icstream = null; + + try { + ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); + + ifstream.init(file, -1, 0, 0); + const replacementChar = Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER; + icstream.init(ifstream, arguments.$charset, 4096, replacementChar); // 4096 bytes buffering + + var buffer = ""; + var str = {}; + while (icstream.readString(4096, str) != 0) + buffer += str.value; + return buffer; + } finally { + if (icstream) + icstream.close(); + if (ifstream) + ifstream.close(); + } +} + +define_keywords("$mode", "$perms", "$charset"); +function write_text_file(file, buf) +{ + keywords(arguments, $charset = "UTF-8", $perms = 0644, $mode = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE); + + var ofstream, ocstream; + try { + ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + ocstream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); + + ofstream.init(file, arguments.$mode, arguments.$perms, 0); + ocstream.init(ofstream, arguments.$charset, 0, 0x0000); + ocstream.writeString(buf); + } finally { + if (ocstream) + ocstream.close(); + if (ofstream) + ofstream.close(); + } +} + +define_keywords("$mode", "$perms"); +function write_binary_file(file, buf) +{ + keywords(arguments, $perms = 0644, $mode = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE); + var stream = null, bstream = null; + + try { + stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + stream.init(file, arguments.$mode, arguments.$perms, 0); + + bstream = binary_output_stream(stream); + bstream.writeBytes(buf, buf.length); + } finally { + if (bstream) + bstream.close(); + if (stream) + stream.close(); + } +} + +function get_file(path) { + var f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + f.initWithPath(path); + return f; +} + +function input_stream_async_wait(stream, callback, requested_count) { + stream = stream.QueryInterface(Ci.nsIAsyncInputStream); + var flags = (requested_count === false) ? Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY : 0; + if (requested_count == null || requested_count == false) + requested_count = 0; + stream.asyncWait({onInputStreamReady: callback}, flags, requested_count, null); +} + +function output_stream_async_wait(stream, callback, requested_count) { + stream = stream.QueryInterface(Ci.nsIAsyncOutputStream); + var flags = (requested_count === false) ? Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY : 0; + if (requested_count == null || requested_count == false) + requested_count = 0; + stream.asyncWait({onOutputStreamReady: callback}, flags, requested_count, null); +} + +function binary_output_stream(stream) { + var s = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + s.setOutputStream(stream); + return s; +} + +function binary_input_stream(stream) { + var s = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + s.setInputStream(stream); + return s; +} + +// callback is called with a single argument, either true if the write succeeded, or false otherwise +function async_binary_write(stream, data, callback) { + function attempt_write() { + try { + while (true) { + if (data.length == 0) { + stream.flush(); + callback(true); + return; + } + var len = stream.write(data, data.length); + if (len == 0) + break; + data = data.substring(len); + } + } + catch (e if (e instanceof Components.Exception) && e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) {} + catch (e) { + callback(false); + return; + } + output_stream_async_wait(stream, attempt_write, data.length); + } + attempt_write(); +} + +/** + * The `str' parameter should be a normal JavaScript string whose char codes specify Unicode code points. + * The return value is a byte string (all char codes are from 0 to 255) equal to the `str' encoded in the specified charset. + * If charset is not specified, it defaults to UTF-8. + */ +function encode_string(str, charset) { + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = charset || "UTF-8"; + var output = converter.ConvertFromUnicode(str); + output += converter.Finish(); + return output; +} + + + +/** + * The `bstr' parameter should be a byte string (all char codes are from 0 to 255). + * The return value is a normal JavaScript unicode sring equal to `bstr' decoded using the specified charset. + * If charset is not specified, it defaults to UTF-8. + */ +function decode_string(bstr, charset) { + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = charset || "UTF-8"; + return converter.ConvertToUnicode(bstr); +} + +function async_binary_string_writer(bstr) { + return function (stream) { + async_binary_write(stream, bstr); + }; +} + +function async_binary_reader(callback) { + return function (stream) { + var bstream = binary_input_stream(stream); + function handler() { + try { + let avail = stream.available(); + if (avail > 0) { + callback(bstream.readBytes(avail)); + } + input_stream_async_wait(stream, handler); + } catch (e) { + callback(null); + } + } + input_stream_async_wait(stream, handler); + }; +} diff --git a/modules/localfile.js b/modules/localfile.js deleted file mode 100644 index 3b24521..0000000 --- a/modules/localfile.js +++ /dev/null @@ -1,270 +0,0 @@ - -//////////// Stolen from venkman - -const PERM_IWOTH = 00002; /* write permission, others */ -const PERM_IWGRP = 00020; /* write permission, group */ - -const MODE_RDONLY = 0x01; -const MODE_WRONLY = 0x02; -const MODE_RDWR = 0x04; -const MODE_CREATE = 0x08; -const MODE_APPEND = 0x10; -const MODE_TRUNCATE = 0x20; -const MODE_SYNC = 0x40; -const MODE_EXCL = 0x80; - - -// XXX: is is necessary to fully qualify fopen as conkeror.fopen? -// -function fopen(path, mode, perms, tmp) -{ - return new LocalFile(path, mode, perms, tmp); -} - -function LocalFile(file, mode, perms, tmp) -{ - const classes = Components.classes; - const interfaces = Components.interfaces; - - const LOCALFILE_CTRID = "@mozilla.org/file/local;1"; - const FILEIN_CTRID = "@mozilla.org/network/file-input-stream;1"; - const FILEOUT_CTRID = "@mozilla.org/network/file-output-stream;1"; - const SCRIPTSTREAM_CTRID = "@mozilla.org/scriptableinputstream;1"; - - const nsIFile = interfaces.nsIFile; - const nsILocalFile = interfaces.nsILocalFile; - const nsIFileOutputStream = interfaces.nsIFileOutputStream; - const nsIFileInputStream = interfaces.nsIFileInputStream; - const nsIScriptableInputStream = interfaces.nsIScriptableInputStream; - - if (typeof perms == "undefined") - perms = 0666 & ~(PERM_IWOTH | PERM_IWGRP); - - if (typeof mode == "string") - { - switch (mode) - { - case ">": - mode = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; - break; - case ">>": - mode = MODE_WRONLY | MODE_CREATE | MODE_APPEND; - break; - case "<": - mode = MODE_RDONLY; - break; - default: - throw "Invalid mode ``" + mode + "''"; - } - } - - if (typeof file == "string") - { - this.localFile = classes[LOCALFILE_CTRID].createInstance(nsILocalFile); - this.localFile.initWithPath(file); - } - else if (file instanceof nsILocalFile) - { - this.localFile = file; - } - else - { - throw "bad type for argument |file|."; - } - - this.path = this.localFile.path; - - if (mode & (MODE_WRONLY | MODE_RDWR)) - { - this.outputStream = - classes[FILEOUT_CTRID].createInstance(nsIFileOutputStream); - this.outputStream.init(this.localFile, mode, perms, 0); - } - - if (mode & (MODE_RDONLY | MODE_RDWR)) - { - var is = classes[FILEIN_CTRID].createInstance(nsIFileInputStream); - is.init(this.localFile, mode, perms, tmp); - this.inputStream = - classes[SCRIPTSTREAM_CTRID].createInstance(nsIScriptableInputStream); - this.inputStream.init(is); - } -} - - -LocalFile.prototype.write = function(buf) -{ - if (!("outputStream" in this)) - throw "file not open for writing."; - - return this.outputStream.write(buf, buf.length); -} - -LocalFile.prototype.read = function(max) -{ - if (!("inputStream" in this)) - throw "file not open for reading."; - - var av = this.inputStream.available(); - if (typeof max == "undefined") - max = av; - - if (!av) - return null; - - var rv = this.inputStream.read(max); - return rv; -} - -LocalFile.prototype.close = function() -{ - if ("outputStream" in this) - this.outputStream.close(); - if ("inputStream" in this) - this.inputStream.close(); -} - -LocalFile.prototype.flush = function() -{ - return this.outputStream.flush(); -} - - - -// file is either a full pathname or an instance of file instanceof nsILocalFile -// reads a file in "text" mode and returns the string -function read_text_file(file, charset) -{ - var ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - var icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); - - charset = charset || "UTF-8"; -/* - if (typeof file == "string") - file = this.getFile(file); - else if (!(file instanceof Components.interfaces.nsILocalFile)) - throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined -*/ - - ifstream.init(file, -1, 0, 0); - const replacementChar = Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER; - icstream.init(ifstream, charset, 4096, replacementChar); // 4096 bytes buffering - - var buffer = ""; - var str = {}; - while (icstream.readString(4096, str) != 0) - buffer += str.value; - - icstream.close(); - ifstream.close(); - - return buffer; -} - -// file is either a full pathname or an instance of file instanceof nsILocalFile -// default permission = 0644, only used when creating a new file, does not change permissions if the file exists -// mode can be ">" or ">>" in addition to the normal MODE_* flags -define_keywords("$mode", "$perms", "$charset"); -function write_text_file(file, buf) -{ - keywords(arguments, $charset = "UTF-8", $perms = 0644); - - var ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - var ocstream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); - -/* - if (typeof file == "string") - file = this.getFile(file); - else if (!(file instanceof Components.interfaces.nsILocalFile)) - throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined -*/ - - var mode = arguments.$mode; - - if (mode == ">>") - mode = MODE_WRONLY | MODE_CREATE | MODE_APPEND; - else if (!mode || mode == ">") - mode = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; - - ofstream.init(file, mode, arguments.$perms, 0); - ocstream.init(ofstream, arguments.$charset, 0, 0x0000); - ocstream.writeString(buf); - - ocstream.close(); - ofstream.close(); -} - - -function get_file(path) { - var f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - f.initWithPath(path); - return f; -} - -minibuffer.prototype.read_file_path = function () { - keywords(arguments, $prompt = "File:", $initial_value = default_directory.path, - $history = "file"); - var result = yield this.read( - $prompt = arguments.$prompt, - $initial_value = arguments.$initial_value, - $history = arguments.$history, - $completer = file_path_completer(), - $auto_complete = true); - yield co_return(result); -} - -minibuffer.prototype.read_file = function () { - var result = yield this.read_file_path(forward_keywords(arguments)); - yield co_return(get_file(result)); -}; - -// FIXME -minibuffer.prototype.read_existing_file = minibuffer.prototype.read_file; - - -minibuffer.prototype.read_file_check_overwrite = function () { - keywords(arguments); - var initial_value = arguments.$initial_value; - do { - var path = yield this.read_file_path(forward_keywords(arguments), $initial_value = initial_value); - - var file = get_file(path); - - if (file.exists()) { - var overwrite = yield this.read_yes_or_no($prompt = "Overwrite existing file " + path + "?"); - if (!overwrite) { - initial_value = path; - continue; - } - } - yield co_return(file); - } while (true); -}; - -function file_path_completer() { - return function(input, pos, conservative) { - var f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - var ents = []; - var dir; - try { - f.initWithPath(input); - if(f.exists() && f.isDirectory()) - dir = f; - else - dir = f.parent; - if(!dir.exists()) return null; - var iter = dir.directoryEntries; - while(iter.hasMoreElements()) { - var e = iter.getNext(); - ents.push(e.path); - } - } catch(e) { - return null; - } - function id(x) { return x}; - return prefix_completer($completions = ents, - $get_string = id, - $get_description = id, - $get_value = id)(input, pos, conservative); - }; -} diff --git a/modules/minibuffer-read-file.js b/modules/minibuffer-read-file.js new file mode 100644 index 0000000..0742161 --- /dev/null +++ b/modules/minibuffer-read-file.js @@ -0,0 +1,69 @@ +require("io.js"); + +minibuffer.prototype.read_file_path = function () { + keywords(arguments, $prompt = "File:", $initial_value = default_directory.path, + $history = "file"); + var result = yield this.read( + $prompt = arguments.$prompt, + $initial_value = arguments.$initial_value, + $history = arguments.$history, + $completer = file_path_completer(), + $auto_complete = true); + yield co_return(result); +} + +minibuffer.prototype.read_file = function () { + var result = yield this.read_file_path(forward_keywords(arguments)); + yield co_return(get_file(result)); +}; + +// FIXME +minibuffer.prototype.read_existing_file = minibuffer.prototype.read_file; + + +minibuffer.prototype.read_file_check_overwrite = function () { + keywords(arguments); + var initial_value = arguments.$initial_value; + do { + var path = yield this.read_file_path(forward_keywords(arguments), $initial_value = initial_value); + + var file = get_file(path); + + if (file.exists()) { + var overwrite = yield this.read_yes_or_no($prompt = "Overwrite existing file " + path + "?"); + if (!overwrite) { + initial_value = path; + continue; + } + } + yield co_return(file); + } while (true); +}; + +function file_path_completer() { + return function(input, pos, conservative) { + var f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + var ents = []; + var dir; + try { + f.initWithPath(input); + if(f.exists() && f.isDirectory()) + dir = f; + else + dir = f.parent; + if(!dir.exists()) return null; + var iter = dir.directoryEntries; + while(iter.hasMoreElements()) { + var e = iter.getNext(); + ents.push(e.path); + } + } catch(e) { + return null; + } + function id(x) { return x}; + return prefix_completer($completions = ents, + $get_string = id, + $get_description = id, + $get_value = id)(input, pos, conservative); + }; +} diff --git a/modules/process.js b/modules/process.js deleted file mode 100644 index ca8ffa6..0000000 --- a/modules/process.js +++ /dev/null @@ -1,155 +0,0 @@ -require("thread.js"); -require("interactive.js"); - -const WINDOWS = (get_os() == "WINNT"); -const POSIX = !WINDOWS; -const PATH = getenv("PATH").split(POSIX ? ":" : ";"); -const path_component_regexp = POSIX ? /^[^/]+$/ : /^[^/\\]+$/; - -function get_file_in_path(name) { - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - if (!path_component_regexp.test(name)) { - // Absolute path - try { - file.initWithPath(name); - if (file.exists()) - return file; - } catch (e) {} - return null; - } else { - // Relative path - for (var i = 0; i < PATH.length; ++i) { - try { - file.initWithPath(PATH[i]); - file.appendRelativePath(name); - if (file.exists()) - return file; - } catch(e) {} - } - } - return null; -} - -function spawn_process_sync(program, args, blocking) { - var process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); - var file = get_file_in_path(program); - if (!file) - throw new Error("invalid executable: " + program); - process.init(file); - return process.run(!!blocking, args, args.length); -} - -function spawn_process(program, args) { - var result = yield in_new_thread(spawn_process_sync, program, args, true); - yield co_return(result); -} - -function shell_command_sync(cwd, cmd, blocking) { - if (POSIX) { - var full_cmd = "cd \"" + shell_quote(cwd) + "\"; " + cmd; - return spawn_process_sync(getenv("SHELL") || "/bin/sh", - ["-c", full_cmd], - blocking); - } else { - - var full_cmd = ""; - if (cwd.match(/[a-z]:/i)) { - full_cmd += cwd.substring(0,2) + " && "; - } - full_cmd += "cd \"" + shell_quote(cwd) + "\" && " + cmd; - - /* Need to convert the single command-line into a list of - * arguments that will then get converted back into a - * command-line by Mozilla. */ - var out = ["/C"]; - var cur_arg = ""; - var quoting = false; - for (var i = 0; i < full_cmd.length; ++i) { - var ch = full_cmd[i]; - if (ch == " ") { - if (quoting) { - cur_arg += ch; - } else { - out.push(cur_arg); - cur_arg = ""; - } - continue; - } - if (ch == "\"") { - quoting = !quoting; - continue; - } - cur_arg += ch; - } - if (cur_arg.length > 0) - out.push(cur_arg); - return spawn_process_sync("cmd.exe", out, blocking); - } -} - -function shell_command(cwd, cmd) { - var result = yield in_new_thread(shell_command_sync, cwd, cmd, true); - yield co_return(result); -} - -var PATH_programs = null; -function get_shell_command_completer() { - if (PATH_programs == null) { - PATH_programs = []; - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - for (var i = 0; i < PATH.length; ++i) { - try { - file.initWithPath(PATH[i]); - var entries = file.directoryEntries; - while (entries.hasMoreElements()) { - var entry = entries.getNext().QueryInterface(Ci.nsIFile); - PATH_programs.push(entry.leafName); - } - } catch (e) {} - } - PATH_programs.sort(); - } - - return prefix_completer($completions = PATH_programs, - $get_string = function (x) { return x; }); -} - -// use default -minibuffer_auto_complete_preferences["shell-command"] = null; - -/* FIXME: support a relative or full path as well as PATH commands */ -define_keywords("$cwd"); -minibuffer.prototype.read_shell_command = function () { - keywords(arguments, $history = "shell-command"); - var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd + "]:"; - var result = yield this.read( - $prompt = prompt, - $history = "shell-command", - $auto_complete = "shell-command", - $select, - $validator = function (x, m) { - var s = x.replace(/^\s+|\s+$/g, ''); - if (s.length == 0) { - m.message("A blank shell command is not allowed."); - return false; - } - return true; - }, - forward_keywords(arguments), - $completer = get_shell_command_completer()); - yield co_return(result); -} - -function shell_command_with_argument_sync(cwd, cmdline, argument, blocking) { - if (!cmdline.match("{}")) { - cmdline = cmdline + " \"" + shell_quote(argument) + "\""; - } else { - cmdline = cmdline.replace("{}", "\"" + shell_quote(argument) + "\""); - } - return shell_command_sync(cwd, cmdline, blocking); -} - -function shell_command_with_argument(cwd, cmdline, argument) { - var result = yield in_new_thread(shell_command_with_argument_sync, cwd, cmdline, argument, true); - yield co_return(result); -} diff --git a/modules/spawn-process.js b/modules/spawn-process.js new file mode 100644 index 0000000..017f3bc --- /dev/null +++ b/modules/spawn-process.js @@ -0,0 +1,593 @@ +require("interactive.js"); +require("io.js"); + +const WINDOWS = (get_os() == "WINNT"); +const POSIX = !WINDOWS; +const PATH = getenv("PATH").split(POSIX ? ":" : ";"); + +const path_component_regexp = POSIX ? /^[^\/]+$/ : /^[^\/\\]+$/; + +function get_file_in_path(name) { + if (name instanceof Ci.nsIFile) { + if (name.exists()) + return name; + return null; + } + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + if (!path_component_regexp.test(name)) { + // Absolute path + try { + file.initWithPath(name); + if (file.exists()) + return file; + } catch (e) {} + return null; + } else { + // Relative path + for (var i = 0; i < PATH.length; ++i) { + try { + file.initWithPath(PATH[i]); + file.appendRelativePath(name); + if (file.exists()) + return file; + } catch(e) {} + } + } + return null; +} + +function spawn_process_internal(program, args, blocking) { + var process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(get_file_in_path(program)); + return process.run(!!blocking, args, args.length); +} + +var PATH_programs = null; +function get_shell_command_completer() { + if (PATH_programs == null) { + PATH_programs = []; + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + for (var i = 0; i < PATH.length; ++i) { + try { + file.initWithPath(PATH[i]); + var entries = file.directoryEntries; + while (entries.hasMoreElements()) { + var entry = entries.getNext().QueryInterface(Ci.nsIFile); + PATH_programs.push(entry.leafName); + } + } catch (e) {} + } + PATH_programs.sort(); + } + + return prefix_completer($completions = PATH_programs, + $get_string = function (x) { return x; }); +} + +// use default +minibuffer_auto_complete_preferences["shell-command"] = null; + +/* FIXME: support a relative or full path as well as PATH commands */ +define_keywords("$cwd"); +minibuffer.prototype.read_shell_command = function () { + keywords(arguments, $history = "shell-command"); + var prompt = arguments.$prompt || "Shell command [" + arguments.$cwd + "]:"; + var result = yield this.read( + $prompt = prompt, + $history = "shell-command", + $auto_complete = "shell-command", + $select, + $validator = function (x, m) { + var s = x.replace(/^\s+|\s+$/g, ''); + if (s.length == 0) { + m.message("A blank shell command is not allowed."); + return false; + } + return true; + }, + forward_keywords(arguments), + $completer = get_shell_command_completer()); + yield co_return(result); +} + +const STDIN_FILENO = 0; +const STDOUT_FILENO = 1; +const STDERR_FILENO = 2; + +var spawn_process_helper_default_fd_wait_timeout = 1000; +var spawn_process_helper_setup_timeout = 2000; +var spawn_process_helper_program = file_locator.get("CurProcD", Ci.nsIFile); +spawn_process_helper_program.append("spawn-process-helper"); + +/** + * @param program_name + * Specifies the full path to the program. + * @param args + * An array of strings to pass as the arguments to the program. + * The first argument should be the program name. These strings must not have + * any NUL bytes in them. + * @param working_dir + * If non-null, switch to the specified path before running the program. + * @param finished_callback + * Called with a single argument, the exit code of the process, as returned by the wait system call. + * @param failure_callback + * Called with a single argument, an exception, if one occurs. + * @param fds + * If non-null, must be an object with only non-negative integer properties set. Each such property + * specifies that the corresponding file descriptor in the spaned process should be redirected. Note that + * 0 corresponds to STDIN, 1 corresponds to STDOUT, and 2 corresponds to STDERR. Note that every redirected + * file descriptor can be used for both input and output, although STDIN, STDOUT, and STDERR are typically + * used only unidirectionally. Each property must be an object itself, with an input and/or output property + * specifying callback functions that are called with an nsIAsyncInputStream or nsIAsyncOutputStream when the + * stream for that file descriptor is available. + * @param fd_wait_timeout + * Specifies the number of milliseconds to wait for the file descriptor redirection sockets to be closed after + * the control socket indicates the process has exited before they are closed forcefully. A negative value + * means to wait indefinitely. If fd_wait_timeout is null, spawn_process_helper_default_fd_wait_timeout + * is used instead. + * + * + * @returns + * A function that can be called to prematurely terminate the spawned process. + */ +function spawn_process(program_name, args, working_dir, + success_callback, failure_callback, fds, + fd_wait_timeout) { + + args = args.slice(); + if (args[0] == null) + args[0] = (program_name instanceof Ci.nsIFile) ? program_name.path : program_name; + + program_name = get_file_in_path(program_name).path; + + const key_length = 100; + const fd_spec_size = 15; + + if (fds == null) + fds = {}; + + if (fd_wait_timeout === undefined) + fd_wait_timeout = spawn_process_helper_default_fd_wait_timeout; + + // Create server socket to listen for connections from the external helper program + try { + var server = Cc['@mozilla.org/network/server-socket;1'].createInstance(Ci.nsIServerSocket); + + var client_key = ""; + var server_key = ""; + // Make sure key does not have any 0 bytes in it. + for (let i = 0; i < key_length; ++i) client_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1); + + // Make sure key does not have any 0 bytes in it. + for (let i = 0; i < key_length; ++i) server_key += String.fromCharCode(Math.floor(Math.random() * 255) + 1); + + var key_file = get_temporary_file("spawn_process_key.dat"); + + var key_file_fd_data = ""; + + // This is the total number of redirected file descriptors. + var total_client_fds = 0; + + // This is the total number of redirected file descriptors that will use a socket connection. + var total_fds = 0; + + for (let i in fds) { + if (fds.hasOwnProperty(i)) { + key_file_fd_data += i + "\0"; + let fd = fds[i]; + if ('file' in fd) { + if (fd.perms == null) + fd.perms = 0666; + key_file_fd_data += fd.file + "\0" + fd.mode + "\0" + fd.perms + "\0"; + delete fds[i]; // Remove it from fds, as we won't need to work with it anymore + } else + ++total_fds; + ++total_client_fds; + } + } + var key_file_data = client_key + "\0" + server_key + "\0" + program_name + "\0" + + (working_dir != null ? working_dir : "") + "\0" + + args.length + "\0" + + args.join("\0") + "\0" + + total_client_fds + "\0" + key_file_fd_data; + + write_binary_file(key_file, key_file_data); + server.init(-1 /* choose a port automatically */, + true /* bind to localhost only */, + -1 /* select backlog size automatically */); + + var unregistered_transports = []; + var registered_transports = []; + + const CONTROL_CONNECTED = 0; + const CONTROL_SENDING_KEY = 1; + const CONTROL_SENT_KEY = 2; + + var control_state = CONTROL_CONNECTED; + var terminate_pending = false; + + var control_transport = null; + + var control_binary_input_stream = null; + var control_output_stream = null, control_input_stream = null; + var exit_status = null; + + function fail(e) { + if (!terminate_pending) { + terminate(); + if (failure_callback) + failure_callback(e); + } + } + + var setup_timer = call_after_timeout(function () { + setup_timer = null; + if (control_state != CONTROL_SENT_KEY) + fail("setup timeout"); + }, spawn_process_helper_setup_timeout); + + function cleanup_server() { + if (server) { + server.close(); + server = null; + } + for (let i in unregistered_transports) { + unregistered_transports[i].close(0); + delete unregistered_transports[i]; + } + } + + function cleanup_fd_sockets() { + for (let i in registered_transports) { + registered_transports[i].transport.close(0); + delete registered_transports[i]; + } + } + + function cleanup_control() { + if (control_transport) { + control_binary_input_stream.close(); + control_binary_input_stream = null; + control_transport.close(0); + control_transport = null; + control_input_stream = null; + control_output_stream = null; + } + } + + function control_send_terminate() { + control_input_stream = null; + control_binary_input_stream.close(); + control_binary_input_stream = null; + async_binary_write(control_output_stream, "\0", function () { + control_output_stream = null; + control_transport.close(0); + control_transport = null; + }); + } + + function terminate() { + if (terminate_pending) + return exit_status; + terminate_pending = true; + if (setup_timer) { + setup_timer.cancel(); + setup_timer = null; + } + cleanup_server(); + cleanup_fd_sockets(); + if (control_transport) { + switch (control_state) { + case CONTROL_SENT_KEY: + control_send_terminate(); + break; + case CONTROL_CONNECTED: + cleanup_control(); + break; + /** + * case CONTROL_SENDING_KEY: in this case once the key + * is sent, the terminate_pending flag will be noticed + * and control_send_terminate will be called, so nothing + * more needs to be done here. + */ + } + } + return exit_status; + } + + function finished() { + // Only call success_callback if terminate was not already called + if (!terminate_pending) { + terminate(); + if (success_callback) + success_callback(exit_status); + } + } + + function wait_for_fd_sockets() { + var remaining_streams = total_fds * 2; + var timer = null; + function handler() { + if (remaining_streams != null) { + --remaining_streams; + if (remaining_streams == 0) { + if (timer) + timer.cancel(); + finished(); + } + } + } + for each (let f in registered_transports) { + input_stream_async_wait(f.input, handler, false /* wait for closure */); + output_stream_async_wait(f.output, handler, false /* wait for closure */); + } + if (fd_wait_timeout != null) { + timer = call_after_timeout(function() { + remaining_streams = null; + finished(); + }, fd_wait_timeout); + } + } + + var control_data = ""; + + function handle_control_input() { + if (terminate_pending) + return; + try { + let avail = control_input_stream.available(); + if (avail > 0) { + control_data += control_binary_input_stream.readBytes(avail); + var off = control_data.indexOf("\0"); + if (off >= 0) { + let message = control_data.substring(0,off); + exit_status = parseInt(message); + cleanup_control(); + /* wait for all fd sockets to close? */ + if (total_fds > 0) + wait_for_fd_sockets(); + else + finished(); + return; + } + } + input_stream_async_wait(control_input_stream, handle_control_input); + } catch (e) { + // Control socket closed: terminate + cleanup_control(); + fail(e); + } + } + + var registered_fds = 0; + + server.asyncListen( + { + onSocketAccepted: function (server, transport) { + unregistered_transports.push(transport); + function remove_from_unregistered() { + var i; + i = unregistered_transports.indexOf(transport); + if (i >= 0) { + unregistered_transports.splice(i, 1); + return true; + } + return false; + } + function close() { + transport.close(0); + remove_from_unregistered(); + } + var received_data = ""; + var header_size = key_length + fd_spec_size; + + var in_stream, bin_stream, out_stream; + + function handle_input() { + if (terminate_pending) + return; + try { + let remaining = header_size - received_data.length; + let avail = in_stream.available(); + if (avail > 0) { + if (avail > remaining) + avail = remaining; + received_data += bin_stream.readBytes(avail); + } + if (received_data.length < header_size) { + input_stream_async_wait(in_stream, handle_input); + } else { + if (received_data.substring(0, key_length) != client_key) + throw "Invalid key"; + } + } catch (e) { + close(); + } + try { + var fdspec = received_data.substring(key_length); + if (fdspec.charCodeAt(0) == 0) { + + // This is the control connection + if (control_transport) + throw "Control transport already exists"; + control_transport = transport; + control_output_stream = out_stream; + control_input_stream = in_stream; + control_binary_input_stream = bin_stream; + remove_from_unregistered(); + } else { + var fd = parseInt(fdspec); + if (!fds.hasOwnProperty(fd) || (fd in registered_transports)) + throw "Invalid fd"; + bin_stream.close(); + bin_stream = null; + registered_transports[fd] = {transport: transport, + input: in_stream, + output: out_stream}; + ++registered_fds; + } + if (control_transport && registered_fds == total_fds) { + cleanup_server(); + control_state = CONTROL_SENDING_KEY; + async_binary_write(control_output_stream, server_key, + function () { + control_state = CONTROL_SENT_KEY; + if (setup_timer) { + setup_timer.cancel(); + setup_timer = null; + } + if (terminate_pending) { + control_send_terminate(); + } else { + for (let i in fds) { + let f = fds[i]; + let t = registered_transports[i]; + if ('input' in f) + f.input(t.input); + if ('output' in f) + f.output(t.output); + } + } + }); + input_stream_async_wait(control_input_stream, handle_control_input); + } + } catch (e) { + fail(e); + } + } + + try { + in_stream = transport.openInputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0); + out_stream = transport.openOutputStream(Ci.nsITransport.OPEN_NON_BLOCKING, 0, 0); + bin_stream = binary_input_stream(in_stream); + input_stream_async_wait(in_stream, handle_input); + } catch (e) { + close(); + } + } + }); + + spawn_process_internal(spawn_process_helper_program, [key_file.path, server.port], false); + return terminate; + } catch (e) { + terminate(); + // Allow the exception to propagate to the caller + throw e; + } +} + +/** + * spawn_process_blind: spawn a process and forget about it + */ +define_keywords("$cwd", "$fds"); +function spawn_process_blind(program_name, args) { + keywords(arguments); + /* Check if we can use spawn_process_internal */ + var cwd = arguments.$cwd; + var fds = arguments.$fds; + if (cwd == null && fds == null && args[0] == null) + spawn_process_internal(program_name, args.slice(1)); + else { + spawn_process(program_name, args, cwd, + null /* success callback */, + null /* failure callback */, + fds); + } +} + + +// Keyword arguments: $cwd, $fds +function spawn_and_wait_for_process(program_name, args) { + keywords(arguments); + var cc = yield CONTINUATION; + spawn_process(program_name, args, arguments.$cwd, + cc, cc.throw, + arguments.$fds); + var result = yield SUSPEND; + yield co_return(result); +} + +// Keyword arguments: $cwd, $fds +function shell_command_blind(cmd) { + keywords(arguments); + /* Check if we can use spawn_process_internal */ + var cwd = arguments.$cwd; + var fds = arguments.$fds; + + var program_name; + var args; + + if (POSIX) { + var full_cmd; + if (cwd) + full_cmd = "cd \"" + shell_quote(cwd) + "\"; " + cmd; + else + full_cmd = cmd; + program_name = getenv("SHELL") || "/bin/sh"; + args = [null, "-c", full_cmd]; + } else { + var full_cmd; + if (cwd) { + full_cmd = ""; + if (cwd.match(/[a-z]:/i)) { + full_cmd += cwd.substring(0,2) + " && "; + } + full_cmd += "cd \"" + shell_quote(cwd) + "\" && " + cmd; + } else + full_cmd = cmd; + + /* Need to convert the single command-line into a list of + * arguments that will then get converted back into a * + command-line by Mozilla. */ + var out = [null, "/C"]; + var cur_arg = ""; + var quoting = false; + for (var i = 0; i < full_cmd.length; ++i) { + var ch = full_cmd[i]; + if (ch == " ") { + if (quoting) { + cur_arg += ch; + } else { + out.push(cur_arg); + cur_arg = ""; + } + continue; + } + if (ch == "\"") { + quoting = !quoting; + continue; + } + cur_arg += ch; + } + if (cur_arg.length > 0) + out.push(cur_arg); + program_name = "cmd.exe"; + args = out; + } + spawn_process_blind(program_name, args, $fds = arguments.$fds); +} + +function substitute_shell_command_argument(cmdline, argument) { + if (!cmdline.match("{}")) + return cmdline + " \"" + shell_quote(argument) + "\""; + else + return cmdline.replace("{}", "\"" + shell_quote(argument) + "\""); +} + +function shell_command_with_argument_blind(command, arg) { + shell_command_blind(substitute_shell_command_argument(command, arg), forward_keywords(arguments)); +} + +// Keyword arguments: $cwd, $fds +function shell_command(command) { + if (!POSIX) + throw new Error("shell_command: Your OS is not yet supported"); + var result = yield spawn_and_wait_for_process(getenv("SHELL") || "/bin/sh", + [null, "-c", command], + forward_keywords(arguments)); + yield co_return(result); +} + +function shell_command_with_argument(command, arg) { + yield co_return((yield shell_command(substitute_shell_command_argument(command, arg), forward_keywords(arguments)))); +} diff --git a/modules/thread.js b/modules/thread.js deleted file mode 100644 index 21ddd83..0000000 --- a/modules/thread.js +++ /dev/null @@ -1,61 +0,0 @@ - -var thread_manager = Cc["@mozilla.org/thread-manager;1"].getService(); - -function thread_callback(run_function) { - this.run_function = run_function; -} -thread_callback.prototype = { - QueryInterface: generate_QI(Ci.nsIRunnable), - run: function () { - this.run_function.call(null); - } -}; - -function call_in_thread(thread, func) { - thread.dispatch(new thread_callback(func), Ci.nsIEventTarget.DISPATCH_NORMAL); -} - -function call_in_new_thread(func, success_cont, error_cont) { - var thread = thread_manager.newThread(0); - var current_thread = thread_manager.currentThread; - call_in_thread(thread, function () { - try { - var result = func(); - call_in_thread(current_thread, function () { - if (success_cont) - success_cont(result); - thread.shutdown(); - }); - } catch (e) { - call_in_thread(current_thread, function () { - if (error_cont) - error_cont(e); - thread.shutdown(); - }); - } - }); -} - -/* Coroutine interface */ -function in_new_thread(f) { - var args = Array.prototype.splice.call(arguments, 1); - var cc = yield CONTINUATION; - var thread = thread_manager.newThread(0); - var current_thread = thread_manager.currentThread; - call_in_thread(thread, function () { - try { - var result = f.apply(null, args); - call_in_thread(current_thread, function () { - thread.shutdown(); - cc(result); - }); - } catch (e) { - call_in_thread(current_thread, function () { - thread.shutdown(); - cc.throw(e); - }); - } - }); - var result = yield SUSPEND; - yield co_return(result); -} diff --git a/modules/utils.js b/modules/utils.js index a8e4d7d..e1e6d76 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -435,7 +435,7 @@ function get_caller_source_code_reference(extra_frames_back) { } if (get_caller_source_code_reference_ignored_functions[f.name]) continue; - return new source_code_reference(f.filename, f.sourceLine); + return new source_code_reference(f.filename, f.lineNumber); } return null; diff --git a/spawn-process-helper.c b/spawn-process-helper.c new file mode 100644 index 0000000..85fb768 --- /dev/null +++ b/spawn-process-helper.c @@ -0,0 +1,345 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void fail(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +void failerr(const char *msg) { + perror(msg); + exit(1); +} + +#define TRY(var, foo) var = foo; while (var == -1) { if(errno != EINTR) failerr(#foo); } + +void *Malloc(size_t count) { void *r = malloc(count); if (!r) fail("malloc"); return r; } + +/** + * read_all: read from the specified file descriptor, returning a + * malloc-allocated buffer containing the data that was read; the + * number of bytes read is stored in *bytes_read. If max_bytes is + * non-negative, it specifies the maximum number of bytes to read. + * Otherwise, read_all reads from the file descriptor until the end of + * file is reached. + */ +char *read_all(int fd, int max_bytes, int *bytes_read) { + int capacity = 256; + if (max_bytes > 0) + capacity = max_bytes; + char *buffer = Malloc(capacity); + int count = 0; + if (max_bytes < 0 || max_bytes > 0) { + while (1) { + int remain; + if (count == capacity) { + capacity *= 2; + buffer = realloc(buffer, capacity); + if (!buffer) + fail("realloc failed"); + } + remain = capacity - count; + if (max_bytes > 0 && remain > max_bytes) + remain = max_bytes; + TRY(remain, read(fd, buffer + count, remain)); + count += remain; + if (remain == 0 || count == max_bytes) + break; + } + } + *bytes_read = count; + return buffer; +} + +/** + * next_term: return the next NUL terminated string from buffer, and + * adjust buffer and len accordingly. + */ +char *next_term(char **buffer, int *len) { + char *p = *buffer; + int x = strnlen(p, *len); + if (x == *len) + fail("error parsing"); + *buffer += x + 1; + *len -= (x + 1); + return p; +} + +struct fd_info { + int desired_fd; + int orig_fd; + char *path; + int open_mode; + int perms; +}; + +void write_all(int fd, const char *buf, int len) { + int result; + do { + TRY(result, write(fd, buf, len)); + buf += result; + len -= result; + } while (len > 0); +} + +/** + * my_connect: Create a connection to the local Conkeror process on + * the specified TCP port. After connecting, the properly formatted + * header specifying the client_key and the "role" (file descriptor or + * -1 to indicate the control socket) are sent as well. The file + * descriptor for the socket is returned. + */ +int my_connect(int port, char *client_key, int role) { + int sockfd; + int result; + struct sockaddr_in sa; + + TRY(sockfd, socket(PF_INET, SOCK_STREAM, 0)); + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + sa.sin_addr.s_addr = inet_addr("127.0.0.1"); + memset(sa.sin_zero, 0, sizeof(sa.sin_zero)); + + TRY(result, connect(sockfd, (struct sockaddr *)&sa, sizeof(sa))); + + /* Send the client key */ + write_all(sockfd, client_key, strlen(client_key)); + + /* Send the role */ + if (role < 0) { + write_all(sockfd, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 15); + } + else { + char buf[16]; + snprintf(buf, 16, "%15d", role); + write_all(sockfd, buf, 15); + } + + return sockfd; +} + +int child_pid = 0; +int control_fd; + +/** + * sigchld_handler: reap any waitable children. Once the child + * process exits, send the exit status back over the control socket, + * then exit. */ +void sigchld_handler(int sig) { + int status; + int pid; + int err; + + while (1) { + pid = waitpid(-1, &status, WNOHANG); + if (pid == 0) + return; + if (pid == -1) { + if (errno == ECHILD) + break; + failerr("waitpid"); + } + + /* Our child process exited */ + if (pid == child_pid && (WIFEXITED(status) || WIFSIGNALED(status))) { + char buf[30]; + snprintf(buf, 30, "%d", status); + write_all(control_fd, buf, strlen(buf) + 1); + exit(0); + } + } +} + +void check_duplicate_fds(struct fd_info *fds, int fd_count) { + int i, j; + for (i = 0; i < fd_count; ++i) { + for (j = i + 1; j < fd_count; ++j) { + if (fds[i].desired_fd == fds[j].desired_fd) + fail("duplicate redirection requested"); + } + } +} + +/** + * setup_fds: Make the requested redirections. For each entry in the + * fds array, rename orig_fd to desired_fd. + */ +void setup_fds(struct fd_info *fds, int fd_count) { + int i, j, result; + for (i = 0; i < fd_count; ++i) { + int fd = fds[i].desired_fd; + /* Check if this file descriptor is still in use by any subsequent + redirection. */ + for (j = i + 1; j < fd_count; ++j) { + if (fd == fds[j].orig_fd) { + /* It is in use. Pick a new file descriptor for fds[j]. */ + int fd_new; + TRY(fd_new, dup(fds[j].orig_fd)); + close(fds[j].orig_fd); + fds[j].orig_fd = fd_new; + break; + } + } + TRY(result, dup2(fd, fds[i].orig_fd)); + close(fds[i].orig_fd); + } +} + +int main(int argc, char **argv) { + + int port; + char *client_key, *server_key, *executable, *workdir; + char **my_argv; + struct fd_info *fds; + int fd_count; + int i; + sigset_t my_mask, my_old_mask; + + if (argc != 3 || (port = atoi(argv[2])) == 0) + fail("Invalid arguments"); + + sigemptyset(&my_mask); + sigaddset(&my_mask, SIGCHLD); + + /* Block SIGPIPE to avoid a signal being generated while writing to a socket */ + signal(SIGPIPE, SIG_IGN); + + /* Parse key file */ + { + char *buf; + int len; + int my_argc; + /* Read the entire file into buf. */ + { + int file; + TRY(file, open(argv[1], O_RDONLY)); + buf = read_all(file, -1, &len); + close(file); + + /* Remove the temporary file */ + remove(argv[1]); + } + client_key = next_term(&buf, &len); + server_key = next_term(&buf, &len); + executable = next_term(&buf, &len); + workdir = next_term(&buf, &len); + my_argc = atoi(next_term(&buf, &len)); + my_argv = Malloc(sizeof(char *) * (my_argc + 1)); + for (i = 0; i < my_argc; ++i) + my_argv[i] = next_term(&buf, &len); + my_argv[my_argc] = NULL; + fd_count = atoi(next_term(&buf, &len)); + if (fd_count < 0) fail("invalid fd count"); + fds = Malloc(sizeof(struct fd_info) * fd_count); + for (i = 0; i < fd_count; ++i) { + fds[i].desired_fd = atoi(next_term(&buf, &len)); + fds[i].path = next_term(&buf, &len); + if (fds[i].path) { + fds[i].open_mode = atoi(next_term(&buf, &len)); + fds[i].perms = atoi(next_term(&buf, &len)); + } + } + if (len != 0) + fail("invalid input file"); + } + + /* Validate the file descriptor redirection request. */ + check_duplicate_fds(fds, fd_count); + + /* Create the control socket connection. */ + control_fd = my_connect(port, client_key, -1); + + /* Create a socket connection or open a local file for each + requested file descriptor redirection. */ + for (i = 0; i < fd_count; ++i) { + if (fds[i].path) { + TRY(fds[i].orig_fd, open(fds[i].path, fds[i].open_mode, fds[i].perms)); + } else { + fds[i].orig_fd = my_connect(port, client_key, fds[i].desired_fd); + } + } + + /* Check server key */ + { + int len = strlen(server_key); + int read_len; + char *buf = read_all(control_fd, len, &read_len); + if (len != read_len || memcmp(buf, server_key, len) != 0) + fail("server key mismatch"); + free(buf); + } + + /* Block SIGCHLD */ + sigprocmask(SIG_BLOCK, &my_mask, &my_old_mask); + + /* Create the child process */ + child_pid = fork(); + if (child_pid == 0) { + int result; + /* Unblock SIGCHLD */ + sigprocmask(SIG_SETMASK, &my_old_mask, NULL); + + /* Reset the SIGPIPE signal handler. */ + signal(SIGPIPE, SIG_DFL); + + /* Close the control socket, as it isn't needed from the child. */ + close(control_fd); + + /* Change to the specified working directory. */ + if (workdir[0] != 0) { + if (chdir(workdir) == -1) + failerr(workdir); + } + + /* Rearrange file descriptors according to the user specification */ + setup_fds(fds, fd_count); + + /* Exec */ + TRY(result, execv(executable, my_argv)); + + } else if (child_pid == -1) { + failerr("fork"); + } else { + /* We are in the parent process */ + char msg; + int count; + + /* Install SIGCHLD handler */ + { + struct sigaction act; + act.sa_handler = sigchld_handler; + sigemptyset(&act.sa_mask); + act.sa_flags = SA_NOCLDSTOP; + sigaction(SIGCHLD, &act, NULL); + } + /* Unblock SIGCHLD */ + sigprocmask(SIG_SETMASK, &my_old_mask, NULL); + + /* Close all of the redirection file descriptors, as we don't need + them from the parent. */ + for (i = 0; i < fd_count; ++i) + close(fds[i].orig_fd); + + /* Wait for a message from the server telling us to exit early. */ + TRY(count, read(control_fd, &msg, 1)); + + if (count == 0) { + /* End of file received: exit without killing child */ + return; + } + + /* Assume msg == 0 until we support more messages */ + TRY(count, kill(child_pid, SIGTERM)); + return; + } +} -- 2.11.4.GIT