diff --git a/b2g/build.mk b/b2g/build.mk index 136eefe3a4a..9eafae5028d 100644 --- a/b2g/build.mk +++ b/b2g/build.mk @@ -2,11 +2,16 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk + installer: @$(MAKE) -C b2g/installer installer package: @$(MAKE) -C b2g/installer +#ifdef FXOS_SIMULATOR + $(PYTHON) $(srcdir)/b2g/simulator/build_xpi.py $(MOZ_PKG_PLATFORM) +#endif install:: @echo 'B2G can't be installed directly.' diff --git a/b2g/simulator/build_xpi.py b/b2g/simulator/build_xpi.py new file mode 100644 index 00000000000..847b7aaf8f7 --- /dev/null +++ b/b2g/simulator/build_xpi.py @@ -0,0 +1,145 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Generate xpi for the simulator addon by: +# - building a special gaia profile for it, as we need: +# * more languages, and, +# * less apps +# than b2g desktop's one +# - retrieve usefull app version metadata from the build system +# - finally, use addon sdk's cfx tool to build the addon xpi +# that ships: +# * a small firefox addon registering to the app manager +# * b2g desktop runtime +# * gaia profile + +import sys, os, re, subprocess +from mozbuild.preprocessor import Preprocessor +from mozbuild.base import MozbuildObject +from mozbuild.util import ensureParentDir +from zipfile import ZipFile +from distutils.version import LooseVersion + +ftp_root_path = "/pub/mozilla.org/labs/fxos-simulator" +UPDATE_LINK = "https://ftp.mozilla.org" + ftp_root_path + "/%(update_path)s/%(xpi_name)s" +UPDATE_URL = "https://ftp.mozilla.org" + ftp_root_path + "/%(update_path)s/update.rdf" +XPI_NAME = "fxos-simulator-%(version)s-%(platform)s.xpi" + +class GaiaBuilder(object): + def __init__(self, build, gaia_path): + self.build = build + self.gaia_path = gaia_path + + def clean(self): + self.build._run_make(target="clean", directory=self.gaia_path) + + def profile(self, env): + self.build._run_make(target="profile", directory=self.gaia_path, num_jobs=1, silent=False, append_env=env) + + def override_prefs(self, srcfile): + # Note that each time we call `make profile` in gaia, a fresh new pref file is created + # cat srcfile >> profile/user.js + with open(os.path.join(self.gaia_path, "profile", "user.js"), "a") as userJs: + userJs.write(open(srcfile).read()) + +def process_package_overload(src, dst, version, app_buildid): + ensureParentDir(dst) + # First replace numeric version like '1.3' + # Then replace with 'slashed' version like '1_4' + # Finally set the full length addon version like 1.3.20131230 + defines = { + "NUM_VERSION": version, + "SLASH_VERSION": version.replace(".", "_"), + "FULL_VERSION": ("%s.%s" % (version, app_buildid)) + } + pp = Preprocessor(defines=defines) + pp.do_filter("substitution") + with open(dst, "w") as output: + with open(src, "r") as input: + pp.processFile(input=input, output=output) + +def add_dir_to_zip(zip, top, pathInZip, blacklist=()): + zf = ZipFile(zip, "a") + for dirpath, subdirs, files in os.walk(top): + dir_relpath = os.path.relpath(dirpath, top) + if dir_relpath.startswith(blacklist): + continue + zf.write(dirpath, os.path.join(pathInZip, dir_relpath)) + for filename in files: + relpath = os.path.join(dir_relpath, filename) + if relpath in blacklist: + continue + zf.write(os.path.join(dirpath, filename), + os.path.join(pathInZip, relpath)) + zf.close() + +def main(platform): + build = MozbuildObject.from_environment() + topsrcdir = build.topsrcdir + distdir = build.distdir + + srcdir = os.path.join(topsrcdir, "b2g", "simulator") + + app_buildid = open(os.path.join(build.topobjdir, "config", "buildid")).read().strip() + + # The simulator uses a shorter version string, + # it only keeps the major version digits A.B + # whereas MOZ_B2G_VERSION is A.B.C.D + b2g_version = build.config_environment.defines["MOZ_B2G_VERSION"].replace('"', '') + version = ".".join(str(n) for n in LooseVersion(b2g_version).version[0:2]) + + # Build a gaia profile specific to the simulator in order to: + # - disable the FTU + # - set custom prefs to enable devtools debugger server + # - set custom settings to disable lockscreen and screen timeout + # - only ship production apps + gaia_path = build.config_environment.substs["GAIADIR"] + builder = GaiaBuilder(build, gaia_path) + builder.clean() + env = { + "NOFTU": "1", + "GAIA_APP_TARGET": "production", + "SETTINGS_PATH": os.path.join(srcdir, "custom-settings.json") + } + builder.profile(env) + builder.override_prefs(os.path.join(srcdir, "custom-prefs.js")) + + # Substitute version strings in the package manifest overload file + manifest_overload = os.path.join(build.topobjdir, "b2g", "simulator", "package-overload.json") + process_package_overload(os.path.join(srcdir, "package-overload.json.in"), + manifest_overload, + version, + app_buildid) + + # Build the simulator addon xpi + xpi_name = XPI_NAME % {"version": version, "platform": platform} + xpi_path = os.path.join(distdir, xpi_name) + + update_path = "%s/%s" % (version, platform) + update_link = UPDATE_LINK % {"update_path": update_path, "xpi_name": xpi_name} + update_url = UPDATE_URL % {"update_path": update_path} + subprocess.check_call([ + build.virtualenv_manager.python_path, os.path.join(topsrcdir, "addon-sdk", "source", "bin", "cfx"), "xpi", \ + "--pkgdir", srcdir, \ + "--manifest-overload", manifest_overload, \ + "--strip-sdk", \ + "--update-link", update_link, \ + "--update-url", update_url, \ + "--static-args", "{\"label\": \"Firefox OS %s\"}" % version, \ + "--output-file", xpi_path \ + ]) + + # Ship b2g-desktop, but prevent its gaia profile to be shipped in the xpi + add_dir_to_zip(xpi_path, os.path.join(distdir, "b2g"), "b2g", ("gaia")) + # Then ship our own gaia profile + add_dir_to_zip(xpi_path, os.path.join(gaia_path, "profile"), "profile") + +if __name__ == '__main__': + if 2 != len(sys.argv): + print("""Usage: + python {0} MOZ_PKG_PLATFORM +""".format(sys.argv[0])) + sys.exit(1) + main(*sys.argv[1:]) + diff --git a/b2g/simulator/custom-prefs.js b/b2g/simulator/custom-prefs.js new file mode 100644 index 00000000000..c3b4cb6fb61 --- /dev/null +++ b/b2g/simulator/custom-prefs.js @@ -0,0 +1,2 @@ +user_pref("devtools.debugger.prompt-connection", false); +user_pref("devtools.debugger.forbid-certified-apps", false); diff --git a/b2g/simulator/custom-settings.json b/b2g/simulator/custom-settings.json new file mode 100644 index 00000000000..ea0264d9a50 --- /dev/null +++ b/b2g/simulator/custom-settings.json @@ -0,0 +1,6 @@ +{ + "debugger.remote-mode": "adb-devtools", + "screen.timeout": 0, + "lockscreen.enabled": false, + "lockscreen.locked": false +} diff --git a/b2g/simulator/icon.png b/b2g/simulator/icon.png new file mode 100644 index 00000000000..c4307fc8418 Binary files /dev/null and b/b2g/simulator/icon.png differ diff --git a/b2g/simulator/icon64.png b/b2g/simulator/icon64.png new file mode 100644 index 00000000000..275cd04a5ea Binary files /dev/null and b/b2g/simulator/icon64.png differ diff --git a/b2g/simulator/lib/main.js b/b2g/simulator/lib/main.js new file mode 100644 index 00000000000..73333e075b4 --- /dev/null +++ b/b2g/simulator/lib/main.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const { Cc, Ci, Cu } = require("chrome"); + +const { SimulatorProcess } = require("./simulator-process"); +const Promise = require("sdk/core/promise"); +const Self = require("sdk/self"); +const System = require("sdk/system"); +const { Simulator } = Cu.import("resource://gre/modules/devtools/Simulator.jsm"); + +let process; + +function launch({ port }) { + // Close already opened simulation + if (process) { + return close().then(launch.bind(null, { port: port })); + } + + process = SimulatorProcess(); + process.remoteDebuggerPort = port; + process.run(); + + return Promise.resolve(); +} + +function close() { + if (!process) { + return Promise.resolve(); + } + let p = process; + process = null; + return p.kill(); +} + + +// Load data generated at build time that +// expose various information about the runtime we ship +let appinfo = System.staticArgs; + +Simulator.register(appinfo.label, { + appinfo: appinfo, + launch: launch, + close: close +}); + +require("sdk/system/unload").when(function () { + Simulator.unregister(appinfo.label); + close(); +}); diff --git a/b2g/simulator/lib/simulator-process.js b/b2g/simulator/lib/simulator-process.js new file mode 100644 index 00000000000..6d35a08330f --- /dev/null +++ b/b2g/simulator/lib/simulator-process.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +'use strict'; + +const { Cc, Ci, Cu, ChromeWorker } = require("chrome"); + +Cu.import("resource://gre/modules/Services.jsm"); + +const { EventTarget } = require("sdk/event/target"); +const { emit, off } = require("sdk/event/core"); +const { Class } = require("sdk/core/heritage"); +const Environment = require("sdk/system/environment").env; +const Runtime = require("sdk/system/runtime"); +const Self = require("sdk/self"); +const URL = require("sdk/url"); +const Subprocess = require("subprocess"); +const Promise = require("sdk/core/promise"); + +const { rootURI: ROOT_URI } = require('@loader/options'); +const PROFILE_URL = ROOT_URI + "profile/"; +const BIN_URL = ROOT_URI + "b2g/"; + +// Log subprocess error and debug messages to the console. This logs messages +// for all consumers of the API. We trim the messages because they sometimes +// have trailing newlines. And note that registerLogHandler actually registers +// an error handler, despite its name. +Subprocess.registerLogHandler( + function(s) console.error("subprocess: " + s.trim()) +); +Subprocess.registerDebugHandler( + function(s) console.debug("subprocess: " + s.trim()) +); + +exports.SimulatorProcess = Class({ + extends: EventTarget, + initialize: function initialize(options) { + EventTarget.prototype.initialize.call(this, options); + + this.on("stdout", function onStdout(data) console.log(data.trim())); + this.on("stderr", function onStderr(data) console.error(data.trim())); + }, + + // check if b2g is running + get isRunning() !!this.process, + + /** + * Start the process and connect the debugger client. + */ + run: function() { + // kill before start if already running + if (this.process != null) { + this.process + .kill() + .then(this.run.bind(this)); + return; + } + + // resolve b2g binaries path (raise exception if not found) + let b2gExecutable = this.b2gExecutable; + + this.once("stdout", function () { + if (Runtime.OS == "Darwin") { + console.debug("WORKAROUND run osascript to show b2g-desktop window"+ + " on Runtime.OS=='Darwin'"); + // Escape double quotes and escape characters for use in AppleScript. + let path = b2gExecutable.path + .replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); + + Subprocess.call({ + command: "/usr/bin/osascript", + arguments: ["-e", 'tell application "' + path + '" to activate'], + }); + } + }); + + let environment; + if (Runtime.OS == "Linux") { + environment = ["TMPDIR=" + Services.dirsvc.get("TmpD",Ci.nsIFile).path]; + if ("DISPLAY" in Environment) { + environment.push("DISPLAY=" + Environment.DISPLAY); + } + } + + // spawn a b2g instance + this.process = Subprocess.call({ + command: b2gExecutable, + arguments: this.b2gArguments, + environment: environment, + + // emit stdout event + stdout: (function(data) { + emit(this, "stdout", data); + }).bind(this), + + // emit stderr event + stderr: (function(data) { + emit(this, "stderr", data); + }).bind(this), + + // on b2g instance exit, reset tracked process, remoteDebuggerPort and + // shuttingDown flag, then finally emit an exit event + done: (function(result) { + console.log(this.b2gFilename + " terminated with " + result.exitCode); + this.process = null; + emit(this, "exit", result.exitCode); + }).bind(this) + }); + }, + + // request a b2g instance kill + kill: function() { + let deferred = Promise.defer(); + if (this.process) { + this.once("exit", (exitCode) => { + this.shuttingDown = false; + deferred.resolve(exitCode); + }); + if (!this.shuttingDown) { + this.shuttingDown = true; + emit(this, "kill", null); + this.process.kill(); + } + return deferred.promise; + } else { + return Promise.resolve(undefined); + } + }, + + // compute current b2g filename + get b2gFilename() { + return this._executable ? this._executableFilename : "B2G"; + }, + + // compute current b2g file handle + get b2gExecutable() { + if (this._executable) return this._executable; + + let bin = URL.toFilename(BIN_URL); + let executables = { + WINNT: "b2g-bin.exe", + Darwin: "Contents/MacOS/b2g-bin", + Linux: "b2g-bin", + }; + + console.log("bin url: "+bin+"/"+executables[Runtime.OS]); + let path = bin + "/" + executables[Runtime.OS]; + + let executable = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + executable.initWithPath(path); + + if (!executable.exists()) { + // B2G binaries not found + throw Error("b2g-desktop Executable not found."); + } + + this._executable = executable; + this._executableFilename = "b2g-bin"; + + return executable; + }, + + // compute b2g CLI arguments + get b2gArguments() { + let args = []; + + let profile = URL.toFilename(PROFILE_URL); + args.push("-profile", profile); + Cu.reportError(profile); + + // NOTE: push dbgport option on the b2g-desktop commandline + args.push("-dbgport", "" + this.remoteDebuggerPort); + + // Ignore eventual zombie instances of b2g that are left over + args.push("-no-remote"); + + return args; + }, +}); + diff --git a/b2g/simulator/package-overload.json.in b/b2g/simulator/package-overload.json.in new file mode 100644 index 00000000000..b107ba98c3f --- /dev/null +++ b/b2g/simulator/package-overload.json.in @@ -0,0 +1,7 @@ +{ + "id": "fxos_@SLASH_VERSION@_simulator@mozilla.org", + "name": "fxos_@SLASH_VERSION@_simulator", + "version": "@FULL_VERSION@", + "fullName": "Firefox OS @NUM_VERSION@ Simulator", + "description": "a Firefox OS @NUM_VERSION@ simulator" +} diff --git a/b2g/simulator/package.json b/b2g/simulator/package.json new file mode 100644 index 00000000000..c2893e4cd4c --- /dev/null +++ b/b2g/simulator/package.json @@ -0,0 +1,37 @@ +{ + "id": "fxos_simulator@mozilla.org", + "name": "fxos_simulator", + "version": "1.0.dev", + "fullName": "Firefox OS Simulator", + "label": "Firefox OS", + "description": "a Firefox OS simulator", + "author": "Myk Melez (https://github.com/mykmelez)", + "contributors": [ + "Alexandre Poirot (https://github.com/ochameau)", + "Anant Narayanan (https://github.com/anantn)", + "Brandon Kase (https://github.com/bkase)", + "Breck Yunits (https://github.com/breck7)", + "César Carruitero (https://github.com/ccarruitero)", + "David Gomes (https://github.com/davidgomes)", + "Fabrice Desré (https://github.com/fabricedesre)", + "Fraser Tweedale (https://github.com/frasertweedale)", + "Harald Kirschner (https://github.com/digitarald)", + "Jérémie Patonnier (https://github.com/JeremiePat)", + "J. Ryan Stinnett (https://github.com/jryans)", + "Kan-Ru Chen (陳侃如) (https://github.com/kanru)", + "Louis Stowasser (https://github.com/louisstow)", + "Luca Greco (https://github.com/rpl)", + "Matthew Claypotch (https://github.com/potch)", + "Matthew Riley MacPherson (https://github.com/tofumatt)", + "Nick Desaulniers (https://github.com/nickdesaulniers)", + "Soumen Ganguly (https://github.com/SoumenG)", + "Sudheesh Singanamalla (https://github.com/sudheesh001)", + "Victor Bjelkholm (https://github.com/VictorBjelkholm)" + ], + "permissions": { + "private-browsing": true + }, + "license": "MPL 2.0", + "unpack": true, + "dependencies": ["subprocess"] +} diff --git a/b2g/simulator/packages/subprocess/README.md b/b2g/simulator/packages/subprocess/README.md new file mode 100644 index 00000000000..7f13df6f9f2 --- /dev/null +++ b/b2g/simulator/packages/subprocess/README.md @@ -0,0 +1,124 @@ +

What's that?

+Simply package enigmail hard work on providing IPC feature in mozilla platform. +So we are able to launch child proccesses from javascript, +and in our case, from addon-sdk libraries :) + +

Sample of code:

+ This object allows to start a process, and read/write data to/from it + using stdin/stdout/stderr streams. + Usage example: + + const subprocess = require("subprocess"); + var p = subprocess.call({ + command: '/bin/foo', + arguments: ['-v', 'foo'], + environment: [ "XYZ=abc", "MYVAR=def" ], + charset: 'UTF-8', + workdir: '/home/foo', + //stdin: "some value to write to stdin\nfoobar", + stdin: function(stdin) { + stdin.write("some value to write to stdin\nfoobar"); + stdin.close(); + }, + stdout: function(data) { + dump("got data on stdout:" + data + "\n"); + }, + stderr: function(data) { + dump("got data on stderr:" + data + "\n"); + }, + done: function(result) { + dump("process terminated with " + result.exitCode + "\n"); + }, + mergeStderr: false + }); + p.wait(); // wait for the subprocess to terminate + // this will block the main thread, + // only do if you can wait that long + + + Description of parameters: + -------------------------- + Apart from , all arguments are optional. + + command: either a |nsIFile| object pointing to an executable file or a + String containing the platform-dependent path to an executable + file. + + arguments: optional string array containing the arguments to the command. + + environment: optional string array containing environment variables to pass + to the command. The array elements must have the form + "VAR=data". Please note that if environment is defined, it + replaces any existing environment variables for the subprocess. + + charset: Output is decoded with given charset and a string is returned. + If charset is undefined, "UTF-8" is used as default. + To get binary data, set this to null and the returned string + is not decoded in any way. + + workdir: optional; String containing the platform-dependent path to a + directory to become the current working directory of the subprocess. + + stdin: optional input data for the process to be passed on standard + input. stdin can either be a string or a function. + A |string| gets written to stdin and stdin gets closed; + A |function| gets passed an object with write and close function. + Please note that the write() function will return almost immediately; + data is always written asynchronously on a separate thread. + + stdout: an optional function that can receive output data from the + process. The stdout-function is called asynchronously; it can be + called mutliple times during the execution of a process. + At a minimum at each occurance of \n or \r. + Please note that null-characters might need to be escaped + with something like 'data.replace(/\0/g, "\\0");'. + + stderr: an optional function that can receive stderr data from the + process. The stderr-function is called asynchronously; it can be + called mutliple times during the execution of a process. Please + note that null-characters might need to be escaped with + something like 'data.replace(/\0/g, "\\0");'. + (on windows it only gets called once right now) + + done: optional function that is called when the process has terminated. + The exit code from the process available via result.exitCode. If + stdout is not defined, then the output from stdout is available + via result.stdout. stderr data is in result.stderr + + mergeStderr: optional boolean value. If true, stderr is merged with stdout; + no data will be provided to stderr. + + + Description of object returned by subprocess.call(...) + ------------------------------------------------------ + The object returned by subprocess.call offers a few methods that can be + executed: + + wait(): waits for the subprocess to terminate. It is not required to use + wait; done will be called in any case when the subprocess terminated. + + kill(hardKill): kill the subprocess. Any open pipes will be closed and + done will be called. + hardKill [ignored on Windows]: + - false: signal the process terminate (SIGTERM) + - true: kill the process (SIGKILL) + + + Other methods in subprocess + --------------------------- + + registerDebugHandler(functionRef): register a handler that is called to get + debugging information + registerLogHandler(functionRef): register a handler that is called to get error + messages + + example: + subprocess.registerLogHandler( function(s) { dump(s); } ); + + +

Credits:

+All enigmail team working on IPC component. + The Initial Developer of this code is Jan Gerber. + Portions created by Jan Gerber , + Patrick Brunschwig (author of almost all code) , + Ramalingam Saravanan (from enigmail team) diff --git a/b2g/simulator/packages/subprocess/lib/subprocess.js b/b2g/simulator/packages/subprocess/lib/subprocess.js new file mode 100644 index 00000000000..0f88a860bd8 --- /dev/null +++ b/b2g/simulator/packages/subprocess/lib/subprocess.js @@ -0,0 +1,1543 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +'use strict'; + +const { Cc, Ci, Cu, ChromeWorker } = require("chrome"); + +Cu.import("resource://gre/modules/ctypes.jsm"); + +const NS_LOCAL_FILE = "@mozilla.org/file/local;1"; + +const Runtime = require("sdk/system/runtime"); +const Environment = require("sdk/system/environment").env; +const DEFAULT_ENVIRONMENT = []; +if (Runtime.OS == "Linux" && "DISPLAY" in Environment) { + DEFAULT_ENVIRONMENT.push("DISPLAY=" + Environment.DISPLAY); +} + +/* +Fake require statements to ensure worker scripts are packaged: +require("subprocess_worker_win.js"); +require("subprocess_worker_unix.js"); +*/ +const URL_PREFIX = module.uri.replace(/subprocess\.js/, ""); +const WORKER_URL_WIN = URL_PREFIX + "subprocess_worker_win.js"; +const WORKER_URL_UNIX = URL_PREFIX + "subprocess_worker_unix.js"; + +//Windows API definitions +if (ctypes.size_t.size == 8) { + var WinABI = ctypes.default_abi; +} else { + var WinABI = ctypes.winapi_abi; +} +const WORD = ctypes.uint16_t; +const DWORD = ctypes.uint32_t; +const LPDWORD = DWORD.ptr; + +const UINT = ctypes.unsigned_int; +const BOOL = ctypes.bool; +const HANDLE = ctypes.size_t; +const HWND = HANDLE; +const HMODULE = HANDLE; +const WPARAM = ctypes.size_t; +const LPARAM = ctypes.size_t; +const LRESULT = ctypes.size_t; +const ULONG_PTR = ctypes.uintptr_t; +const PVOID = ctypes.voidptr_t; +const LPVOID = PVOID; +const LPCTSTR = ctypes.jschar.ptr; +const LPCWSTR = ctypes.jschar.ptr; +const LPTSTR = ctypes.jschar.ptr; +const LPSTR = ctypes.char.ptr; +const LPCSTR = ctypes.char.ptr; +const LPBYTE = ctypes.char.ptr; + +const CREATE_NEW_CONSOLE = 0x00000010; +const CREATE_NO_WINDOW = 0x08000000; +const CREATE_UNICODE_ENVIRONMENT = 0x00000400; +const STARTF_USESHOWWINDOW = 0x00000001; +const STARTF_USESTDHANDLES = 0x00000100; +const SW_HIDE = 0; +const DUPLICATE_SAME_ACCESS = 0x00000002; +const STILL_ACTIVE = 259; +const INFINITE = DWORD(0xFFFFFFFF); +const WAIT_TIMEOUT = 0x00000102; + +/* +typedef struct _SECURITY_ATTRIBUTES { + DWORD nLength; + LPVOID lpSecurityDescriptor; + BOOL bInheritHandle; +} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; +*/ +const SECURITY_ATTRIBUTES = new ctypes.StructType("SECURITY_ATTRIBUTES", [ + {"nLength": DWORD}, + {"lpSecurityDescriptor": LPVOID}, + {"bInheritHandle": BOOL}, +]); + +/* +typedef struct _STARTUPINFO { + DWORD cb; + LPTSTR lpReserved; + LPTSTR lpDesktop; + LPTSTR lpTitle; + DWORD dwX; + DWORD dwY; + DWORD dwXSize; + DWORD dwYSize; + DWORD dwXCountChars; + DWORD dwYCountChars; + DWORD dwFillAttribute; + DWORD dwFlags; + WORD wShowWindow; + WORD cbReserved2; + LPBYTE lpReserved2; + HANDLE hStdInput; + HANDLE hStdOutput; + HANDLE hStdError; +} STARTUPINFO, *LPSTARTUPINFO; +*/ +const STARTUPINFO = new ctypes.StructType("STARTUPINFO", [ + {"cb": DWORD}, + {"lpReserved": LPTSTR}, + {"lpDesktop": LPTSTR}, + {"lpTitle": LPTSTR}, + {"dwX": DWORD}, + {"dwY": DWORD}, + {"dwXSize": DWORD}, + {"dwYSize": DWORD}, + {"dwXCountChars": DWORD}, + {"dwYCountChars": DWORD}, + {"dwFillAttribute": DWORD}, + {"dwFlags": DWORD}, + {"wShowWindow": WORD}, + {"cbReserved2": WORD}, + {"lpReserved2": LPBYTE}, + {"hStdInput": HANDLE}, + {"hStdOutput": HANDLE}, + {"hStdError": HANDLE}, +]); + +/* +typedef struct _PROCESS_INFORMATION { + HANDLE hProcess; + HANDLE hThread; + DWORD dwProcessId; + DWORD dwThreadId; +} PROCESS_INFORMATION, *LPPROCESS_INFORMATION; +*/ +const PROCESS_INFORMATION = new ctypes.StructType("PROCESS_INFORMATION", [ + {"hProcess": HANDLE}, + {"hThread": HANDLE}, + {"dwProcessId": DWORD}, + {"dwThreadId": DWORD}, +]); + +/* +typedef struct _OVERLAPPED { + ULONG_PTR Internal; + ULONG_PTR InternalHigh; + union { + struct { + DWORD Offset; + DWORD OffsetHigh; + }; + PVOID Pointer; + }; + HANDLE hEvent; +} OVERLAPPED, *LPOVERLAPPED; +*/ +const OVERLAPPED = new ctypes.StructType("OVERLAPPED"); + +//UNIX definitions +const pid_t = ctypes.int32_t; +const WNOHANG = 1; +const F_GETFD = 1; +const F_SETFL = 4; + +const LIBNAME = 0; +const O_NONBLOCK = 1; +const RLIM_T = 2; +const RLIMIT_NOFILE = 3; + +function getPlatformValue(valueType) { + + if (! gXulRuntime) + gXulRuntime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + + const platformDefaults = { + // Windows API: + 'winnt': [ 'kernel32.dll' ], + + // Unix API: + // library name O_NONBLOCK RLIM_T RLIMIT_NOFILE + 'darwin': [ 'libc.dylib', 0x04 , ctypes.uint64_t , 8 ], + 'linux': [ 'libc.so.6', 2024 , ctypes.unsigned_long, 7 ], + 'freebsd': [ 'libc.so.7', 0x04 , ctypes.int64_t , 8 ], + 'openbsd': [ 'libc.so.61.0', 0x04 , ctypes.int64_t , 8 ], + 'sunos': [ 'libc.so', 0x80 , ctypes.unsigned_long, 5 ] + } + + return platformDefaults[gXulRuntime.OS.toLowerCase()][valueType]; +} + + +var gDebugFunc = null, + gLogFunc = null, + gXulRuntime = null; + +function LogError(s) { + if (gLogFunc) + gLogFunc(s); + else + dump(s); +} + +function debugLog(s) { + if (gDebugFunc) + gDebugFunc(s); +} + +function setTimeout(callback, timeout) { + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(callback, timeout, Ci.nsITimer.TYPE_ONE_SHOT); +}; + +function getBytes(data) { + var string = ''; + data.forEach(function(x) { string += String.fromCharCode(x < 0 ? x + 256 : x) }); + return string; +} + +function readString(data, length, charset) { + var string = '', bytes = []; + for(var i = 0;i < length; i++) { + if(data[i] == 0 && charset !== null) // stop on NULL character for non-binary data + break + bytes.push(data[i]); + } + if (!bytes || bytes.length == 0) + return string; + if(charset === null) { + return bytes; + } + return convertBytes(bytes, charset); +} + +function convertBytes(bytes, charset) { + var string = ''; + charset = charset || 'UTF-8'; + var unicodeConv = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .getService(Ci.nsIScriptableUnicodeConverter); + try { + unicodeConv.charset = charset; + string = unicodeConv.convertFromByteArray(bytes, bytes.length); + } catch (ex) { + LogError("String conversion failed: "+ex.toString()+"\n") + string = ''; + } + string += unicodeConv.Finish(); + return string; +} + + +// temporary solution for removal of nsILocalFile +function getLocalFileApi() { + if ("nsILocalFile" in Ci) { + return Ci.nsILocalFile; + } + else + return Ci.nsIFile; +} + +function getCommandStr(command) { + let commandStr = null; + if (typeof(command) == "string") { + let file = Cc[NS_LOCAL_FILE].createInstance(getLocalFileApi()); + file.initWithPath(command); + if (! (file.isExecutable() && file.isFile())) + throw("File '"+command+"' is not an executable file"); + commandStr = command; + } + else { + if (! (command.isExecutable() && command.isFile())) + throw("File '"+command.path+"' is not an executable file"); + commandStr = command.path; + } + + return commandStr; +} + +function getWorkDir(workdir) { + let workdirStr = null; + if (typeof(workdir) == "string") { + let file = Cc[NS_LOCAL_FILE].createInstance(getLocalFileApi()); + file.initWithPath(workdir); + if (! (file.isDirectory())) + throw("Directory '"+workdir+"' does not exist"); + workdirStr = workdir; + } + else if (workdir) { + if (! workdir.isDirectory()) + throw("Directory '"+workdir.path+"' does not exist"); + workdirStr = workdir.path; + } + return workdirStr; +} + + +var subprocess = { + call: function(options) { + options.mergeStderr = options.mergeStderr || false; + options.workdir = options.workdir || null; + options.environment = options.environment || DEFAULT_ENVIRONMENT; + if (options.arguments) { + var args = options.arguments; + options.arguments = []; + args.forEach(function(argument) { + options.arguments.push(argument); + }); + } else { + options.arguments = []; + } + + options.libc = getPlatformValue(LIBNAME); + + if (gXulRuntime.OS.substring(0, 3) == "WIN") { + return subprocess_win32(options); + } else { + return subprocess_unix(options); + } + + }, + registerDebugHandler: function(func) { + gDebugFunc = func; + }, + registerLogHandler: function(func) { + gLogFunc = func; + } +}; + + + +function subprocess_win32(options) { + var kernel32dll = ctypes.open(options.libc), + hChildProcess, + active = true, + done = false, + exitCode = -1, + child = {}, + stdinWorker = null, + stdoutWorker = null, + stderrWorker = null, + pendingWriteCount = 0, + readers = options.mergeStderr ? 1 : 2, + stdinOpenState = 2, + error = '', + output = ''; + + // stdin pipe states + const OPEN = 2; + const CLOSEABLE = 1; + const CLOSED = 0; + + //api declarations + /* + BOOL WINAPI CloseHandle( + __in HANDLE hObject + ); + */ + var CloseHandle = kernel32dll.declare("CloseHandle", + WinABI, + BOOL, + HANDLE + ); + + /* + BOOL WINAPI CreateProcess( + __in_opt LPCTSTR lpApplicationName, + __inout_opt LPTSTR lpCommandLine, + __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, + __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, + __in BOOL bInheritHandles, + __in DWORD dwCreationFlags, + __in_opt LPVOID lpEnvironment, + __in_opt LPCTSTR lpCurrentDirectory, + __in LPSTARTUPINFO lpStartupInfo, + __out LPPROCESS_INFORMATION lpProcessInformation + ); + */ + var CreateProcessW = kernel32dll.declare("CreateProcessW", + WinABI, + BOOL, + LPCTSTR, + LPTSTR, + SECURITY_ATTRIBUTES.ptr, + SECURITY_ATTRIBUTES.ptr, + BOOL, + DWORD, + LPVOID, + LPCTSTR, + STARTUPINFO.ptr, + PROCESS_INFORMATION.ptr + ); + +// /* +// BOOL WINAPI ReadFile( +// __in HANDLE hFile, +// __out LPVOID ReadFileBuffer, +// __in DWORD nNumberOfBytesToRead, +// __out_opt LPDWORD lpNumberOfBytesRead, +// __inout_opt LPOVERLAPPED lpOverlapped +// ); +// */ +// var ReadFileBufferSize = 1024, +// ReadFileBuffer = ctypes.char.array(ReadFileBufferSize), +// ReadFile = kernel32dll.declare("ReadFile", +// WinABI, +// BOOL, +// HANDLE, +// ReadFileBuffer, +// DWORD, +// LPDWORD, +// OVERLAPPED.ptr +// ); +// +// /* +// BOOL WINAPI PeekNamedPipe( +// __in HANDLE hNamedPipe, +// __out_opt LPVOID lpBuffer, +// __in DWORD nBufferSize, +// __out_opt LPDWORD lpBytesRead, +// __out_opt LPDWORD lpTotalBytesAvail, +// __out_opt LPDWORD lpBytesLeftThisMessage +// ); +// */ +// var PeekNamedPipe = kernel32dll.declare("PeekNamedPipe", +// WinABI, +// BOOL, +// HANDLE, +// ReadFileBuffer, +// DWORD, +// LPDWORD, +// LPDWORD, +// LPDWORD +// ); +// +// /* +// BOOL WINAPI WriteFile( +// __in HANDLE hFile, +// __in LPCVOID lpBuffer, +// __in DWORD nNumberOfBytesToWrite, +// __out_opt LPDWORD lpNumberOfBytesWritten, +// __inout_opt LPOVERLAPPED lpOverlapped +// ); +// */ +// var WriteFile = kernel32dll.declare("WriteFile", +// WinABI, +// BOOL, +// HANDLE, +// ctypes.char.ptr, +// DWORD, +// LPDWORD, +// OVERLAPPED.ptr +// ); + + /* + BOOL WINAPI CreatePipe( + __out PHANDLE hReadPipe, + __out PHANDLE hWritePipe, + __in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes, + __in DWORD nSize + ); + */ + var CreatePipe = kernel32dll.declare("CreatePipe", + WinABI, + BOOL, + HANDLE.ptr, + HANDLE.ptr, + SECURITY_ATTRIBUTES.ptr, + DWORD + ); + + /* + HANDLE WINAPI GetCurrentProcess(void); + */ + var GetCurrentProcess = kernel32dll.declare("GetCurrentProcess", + WinABI, + HANDLE + ); + + /* + DWORD WINAPI GetLastError(void); + */ + var GetLastError = kernel32dll.declare("GetLastError", + WinABI, + DWORD + ); + + /* + BOOL WINAPI DuplicateHandle( + __in HANDLE hSourceProcessHandle, + __in HANDLE hSourceHandle, + __in HANDLE hTargetProcessHandle, + __out LPHANDLE lpTargetHandle, + __in DWORD dwDesiredAccess, + __in BOOL bInheritHandle, + __in DWORD dwOptions + ); + */ + var DuplicateHandle = kernel32dll.declare("DuplicateHandle", + WinABI, + BOOL, + HANDLE, + HANDLE, + HANDLE, + HANDLE.ptr, + DWORD, + BOOL, + DWORD + ); + + + /* + BOOL WINAPI GetExitCodeProcess( + __in HANDLE hProcess, + __out LPDWORD lpExitCode + ); + */ + var GetExitCodeProcess = kernel32dll.declare("GetExitCodeProcess", + WinABI, + BOOL, + HANDLE, + LPDWORD + ); + + /* + DWORD WINAPI WaitForSingleObject( + __in HANDLE hHandle, + __in DWORD dwMilliseconds + ); + */ + var WaitForSingleObject = kernel32dll.declare("WaitForSingleObject", + WinABI, + DWORD, + HANDLE, + DWORD + ); + + /* + BOOL WINAPI TerminateProcess( + __in HANDLE hProcess, + __in UINT uExitCode + ); + */ + var TerminateProcess = kernel32dll.declare("TerminateProcess", + WinABI, + BOOL, + HANDLE, + UINT + ); + + //functions + function popen(command, workdir, args, environment, child) { + //escape arguments + args.unshift(command); + for (var i = 0; i < args.length; i++) { + if (typeof args[i] != "string") { args[i] = args[i].toString(); } + /* quote arguments with spaces */ + if (args[i].match(/\s/)) { + args[i] = "\"" + args[i] + "\""; + } + /* If backslash is followed by a quote, double it */ + args[i] = args[i].replace(/\\\"/g, "\\\\\""); + } + command = args.join(' '); + + environment = environment || []; + if(environment.length) { + //An environment block consists of + //a null-terminated block of null-terminated strings. + //Using CREATE_UNICODE_ENVIRONMENT so needs to be jschar + environment = ctypes.jschar.array()(environment.join('\0') + '\0'); + } else { + environment = null; + } + + var hOutputReadTmp = new HANDLE(), + hOutputRead = new HANDLE(), + hOutputWrite = new HANDLE(); + + var hErrorRead = new HANDLE(), + hErrorReadTmp = new HANDLE(), + hErrorWrite = new HANDLE(); + + var hInputRead = new HANDLE(), + hInputWriteTmp = new HANDLE(), + hInputWrite = new HANDLE(); + + // Set up the security attributes struct. + var sa = new SECURITY_ATTRIBUTES(); + sa.nLength = SECURITY_ATTRIBUTES.size; + sa.lpSecurityDescriptor = null; + sa.bInheritHandle = true; + + // Create output pipe. + + if(!CreatePipe(hOutputReadTmp.address(), hOutputWrite.address(), sa.address(), 0)) + LogError('CreatePipe hOutputReadTmp failed'); + + if(options.mergeStderr) { + // Create a duplicate of the output write handle for the std error + // write handle. This is necessary in case the child application + // closes one of its std output handles. + if (!DuplicateHandle(GetCurrentProcess(), hOutputWrite, + GetCurrentProcess(), hErrorWrite.address(), 0, + true, DUPLICATE_SAME_ACCESS)) + LogError("DuplicateHandle hOutputWrite failed"); + } else { + // Create error pipe. + if(!CreatePipe(hErrorReadTmp.address(), hErrorWrite.address(), sa.address(), 0)) + LogError('CreatePipe hErrorReadTmp failed'); + } + + // Create input pipe. + if (!CreatePipe(hInputRead.address(),hInputWriteTmp.address(),sa.address(), 0)) + LogError("CreatePipe hInputRead failed"); + + // Create new output/error read handle and the input write handles. Set + // the Properties to FALSE. Otherwise, the child inherits the + // properties and, as a result, non-closeable handles to the pipes + // are created. + if (!DuplicateHandle(GetCurrentProcess(), hOutputReadTmp, + GetCurrentProcess(), + hOutputRead.address(), // Address of new handle. + 0, false, // Make it uninheritable. + DUPLICATE_SAME_ACCESS)) + LogError("DupliateHandle hOutputReadTmp failed"); + + if(!options.mergeStderr) { + if (!DuplicateHandle(GetCurrentProcess(), hErrorReadTmp, + GetCurrentProcess(), + hErrorRead.address(), // Address of new handle. + 0, false, // Make it uninheritable. + DUPLICATE_SAME_ACCESS)) + LogError("DupliateHandle hErrorReadTmp failed"); + } + if (!DuplicateHandle(GetCurrentProcess(), hInputWriteTmp, + GetCurrentProcess(), + hInputWrite.address(), // Address of new handle. + 0, false, // Make it uninheritable. + DUPLICATE_SAME_ACCESS)) + LogError("DupliateHandle hInputWriteTmp failed"); + + // Close inheritable copies of the handles. + if (!CloseHandle(hOutputReadTmp)) LogError("CloseHandle hOutputReadTmp failed"); + if(!options.mergeStderr) + if (!CloseHandle(hErrorReadTmp)) LogError("CloseHandle hErrorReadTmp failed"); + if (!CloseHandle(hInputWriteTmp)) LogError("CloseHandle failed"); + + var pi = new PROCESS_INFORMATION(); + var si = new STARTUPINFO(); + + si.cb = STARTUPINFO.size; + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = hInputRead; + si.hStdOutput = hOutputWrite; + si.hStdError = hErrorWrite; + + // Launch the process + if(!CreateProcessW(null, // executable name + command, // command buffer + null, // process security attribute + null, // thread security attribute + true, // inherits system handles + CREATE_UNICODE_ENVIRONMENT|CREATE_NO_WINDOW, // process flags + environment, // envrionment block + workdir, // set as current directory + si.address(), // (in) startup information + pi.address() // (out) process information + )) + throw("Fatal - Could not launch subprocess '"+command+"'"); + + // Close any unnecessary handles. + if (!CloseHandle(pi.hThread)) + LogError("CloseHandle pi.hThread failed"); + + // Close pipe handles (do not continue to modify the parent). + // You need to make sure that no handles to the write end of the + // output pipe are maintained in this process or else the pipe will + // not close when the child process exits and the ReadFile will hang. + if (!CloseHandle(hInputRead)) LogError("CloseHandle hInputRead failed"); + if (!CloseHandle(hOutputWrite)) LogError("CloseHandle hOutputWrite failed"); + if (!CloseHandle(hErrorWrite)) LogError("CloseHandle hErrorWrite failed"); + + //return values + child.stdin = hInputWrite; + child.stdout = hOutputRead; + child.stderr = options.mergeStderr ? undefined : hErrorRead; + child.process = pi.hProcess; + return pi.hProcess; + } + + /* + * createStdinWriter () + * + * Create a ChromeWorker object for writing data to the subprocess' stdin + * pipe. The ChromeWorker object lives on a separate thread; this avoids + * internal deadlocks. + */ + function createStdinWriter() { + debugLog("Creating new stdin worker\n"); + stdinWorker = new ChromeWorker(WORKER_URL_WIN); + stdinWorker.onmessage = function(event) { + switch(event.data) { + case "WriteOK": + pendingWriteCount--; + debugLog("got OK from stdinWorker - remaining count: "+pendingWriteCount+"\n"); + break; + case "ClosedOK": + stdinOpenState = CLOSED; + debugLog("Stdin pipe closed\n"); + break; + default: + debugLog("got msg from stdinWorker: "+event.data+"\n"); + } + } + stdinWorker.onerror = function(error) { + pendingWriteCount--; + LogError("got error from stdinWorker: "+error.message+"\n"); + } + + stdinWorker.postMessage({msg: "init", libc: options.libc}); + } + + /* + * writeStdin() + * @data: String containing the data to write + * + * Write data to the subprocess' stdin (equals to sending a request to the + * ChromeWorker object to write the data). + */ + function writeStdin(data) { + ++pendingWriteCount; + debugLog("sending "+data.length+" bytes to stdinWorker\n"); + var pipePtr = parseInt(ctypes.cast(child.stdin.address(), ctypes.uintptr_t).value); + + stdinWorker.postMessage({ + msg: 'write', + pipe: pipePtr, + data: data + }); + } + + /* + * closeStdinHandle() + * + * Close the stdin pipe, either directly or by requesting the ChromeWorker to + * close the pipe. The ChromeWorker will only close the pipe after the last write + * request process is done. + */ + + function closeStdinHandle() { + debugLog("trying to close stdin\n"); + if (stdinOpenState != OPEN) return; + stdinOpenState = CLOSEABLE; + + if (stdinWorker) { + debugLog("sending close stdin to worker\n"); + var pipePtr = parseInt(ctypes.cast(child.stdin.address(), ctypes.uintptr_t).value); + stdinWorker.postMessage({ + msg: 'close', + pipe: pipePtr + }); + } + else { + stdinOpenState = CLOSED; + debugLog("Closing Stdin\n"); + CloseHandle(child.stdin) || LogError("CloseHandle hInputWrite failed"); + } + } + + + /* + * createReader(pipe, name) + * + * @pipe: handle to the pipe + * @name: String containing the pipe name (stdout or stderr) + * + * Create a ChromeWorker object for reading data asynchronously from + * the pipe (i.e. on a separate thread), and passing the result back to + * the caller. + */ + function createReader(pipe, name, callbackFunc) { + var worker = new ChromeWorker(WORKER_URL_WIN); + worker.onmessage = function(event) { + switch(event.data.msg) { + case "data": + debugLog("got "+event.data.count+" bytes from "+name+"\n"); + var data = ''; + if (options.charset === null) { + data = getBytes(event.data.data); + } + else { + try { + data = convertBytes(event.data.data, options.charset); + } + catch(ex) { + console.warn("error decoding output: " + ex); + data = getBytes(event.data.data); + } + } + + callbackFunc(data); + break; + case "done": + debugLog("Pipe "+name+" closed\n"); + --readers; + if (readers == 0) cleanup(); + break; + default: + debugLog("Got msg from "+name+": "+event.data.data+"\n"); + } + } + + worker.onerror = function(errorMsg) { + LogError("Got error from "+name+": "+errorMsg.message); + } + + var pipePtr = parseInt(ctypes.cast(pipe.address(), ctypes.uintptr_t).value); + + worker.postMessage({ + msg: 'read', + pipe: pipePtr, + libc: options.libc, + charset: options.charset === null ? "null" : options.charset, + name: name + }); + + return worker; + } + + /* + * readPipes() + * + * Open the pipes for reading from stdout and stderr + */ + function readPipes() { + + stdoutWorker = createReader(child.stdout, "stdout", function (data) { + if(options.stdout) { + setTimeout(function() { + options.stdout(data); + }, 0); + } else { + output += data; + } + }); + + + if (!options.mergeStderr) stderrWorker = createReader(child.stderr, "stderr", function (data) { + if(options.stderr) { + setTimeout(function() { + options.stderr(data); + }, 0); + } else { + error += data; + } + }); + } + + /* + * cleanup() + * + * close stdin if needed, get the exit code from the subprocess and invoke + * the caller's done() function. + * + * Note: because stdout() and stderr() are called using setTimeout, we need to + * do the same here in order to guarantee the message sequence. + */ + function cleanup() { + debugLog("Cleanup called\n"); + if(active) { + active = false; + + closeStdinHandle(); // should only be required in case of errors + + var exit = new DWORD(); + GetExitCodeProcess(child.process, exit.address()); + exitCode = exit.value; + + if (stdinWorker) + stdinWorker.postMessage({msg: 'stop'}) + + setTimeout(function _done() { + if (options.done) { + try { + options.done({ + exitCode: exitCode, + stdout: output, + stderr: error, + }); + } + catch (ex) { + // prevent from blocking if options.done() throws an error + done = true; + throw ex; + } + } + done = true; + }, 0); + kernel32dll.close(); + } + } + + var cmdStr = getCommandStr(options.command); + var workDir = getWorkDir(options.workdir); + + //main + hChildProcess = popen(cmdStr, workDir, options.arguments, options.environment, child); + + readPipes(); + + if (options.stdin) { + createStdinWriter(); + + if(typeof(options.stdin) == 'function') { + try { + options.stdin({ + write: function(data) { + writeStdin(data); + }, + close: function() { + closeStdinHandle(); + } + }); + } + catch (ex) { + // prevent from failing if options.stdin() throws an exception + closeStdinHandle(); + throw ex; + } + } else { + writeStdin(options.stdin); + closeStdinHandle(); + } + } + else + closeStdinHandle(); + + return { + kill: function(hardKill) { + // hardKill is currently ignored on Windows + var r = !!TerminateProcess(child.process, 255); + cleanup(-1); + return r; + }, + wait: function() { + // wait for async operations to complete + var thread = Cc['@mozilla.org/thread-manager;1'].getService(Ci.nsIThreadManager).currentThread; + while (!done) thread.processNextEvent(true); + + return exitCode; + } + } +} + + +function subprocess_unix(options) { + // stdin pipe states + const OPEN = 2; + const CLOSEABLE = 1; + const CLOSED = 0; + + var libc = ctypes.open(options.libc), + active = true, + done = false, + exitCode = -1, + workerExitCode = 0, + child = {}, + pid = -1, + stdinWorker = null, + stdoutWorker = null, + stderrWorker = null, + pendingWriteCount = 0, + readers = options.mergeStderr ? 1 : 2, + stdinOpenState = OPEN, + error = '', + output = ''; + + //api declarations + + //pid_t fork(void); + var fork = libc.declare("fork", + ctypes.default_abi, + pid_t + ); + + //NULL terminated array of strings, argv[0] will be command >> + 2 + var argv = ctypes.char.ptr.array(options.arguments.length + 2); + var envp = ctypes.char.ptr.array(options.environment.length + 1); + + // posix_spawn_file_actions_t is a complex struct that may be different on + // each platform. We do not care about its attributes, we don't need to + // get access to them, but we do need to allocate the right amount + // of memory for it. + // Bug 936297 - Use OS.File internals to fetch + // sizeof(posix_spawn_file_actions_t) + var { OS } = Cu.import("resource://gre/modules/osfile.jsm", {}); + var sizeof_file_actions_t = OS.Constants.libc.OSFILE_SIZEOF_DIRENT; + var posix_spawn_file_actions_t = ctypes.uint8_t.array(sizeof_file_actions_t); + + //int posix_spawn(pid_t *restrict pid, const char *restrict path, + // const posix_spawn_file_actions_t *file_actions, + // const posix_spawnattr_t *restrict attrp, + // char *const argv[restrict], char *const envp[restrict]); + var posix_spawn = libc.declare("posix_spawn", + ctypes.default_abi, + ctypes.int, + pid_t.ptr, + ctypes.char.ptr, + posix_spawn_file_actions_t.ptr, + ctypes.voidptr_t, + argv, + envp + ); + + //int posix_spawn_file_actions_init(posix_spawn_file_actions_t *file_actions); + var posix_spawn_file_actions_init = libc.declare("posix_spawn_file_actions_init", + ctypes.default_abi, + ctypes.int, + posix_spawn_file_actions_t.ptr + ); + + //int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *file_actions); + var posix_spawn_file_actions_destroy = libc.declare("posix_spawn_file_actions_destroy", + ctypes.default_abi, + ctypes.int, + posix_spawn_file_actions_t.ptr + ); + + // int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t * + // file_actions, int fildes, int newfildes); + var posix_spawn_file_actions_adddup2 = libc.declare("posix_spawn_file_actions_adddup2", + ctypes.default_abi, + ctypes.int, + posix_spawn_file_actions_t.ptr, + ctypes.int, + ctypes.int + ); + + // int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t * + // file_actions, int fildes); + var posix_spawn_file_actions_addclose = libc.declare("posix_spawn_file_actions_addclose", + ctypes.default_abi, + ctypes.int, + posix_spawn_file_actions_t.ptr, + ctypes.int + ); + + //int pipe(int pipefd[2]); + var pipefd = ctypes.int.array(2); + var pipe = libc.declare("pipe", + ctypes.default_abi, + ctypes.int, + pipefd + ); + + //int close(int fd); + var close = libc.declare("close", + ctypes.default_abi, + ctypes.int, + ctypes.int + ); + + //pid_t waitpid(pid_t pid, int *status, int options); + var waitpid = libc.declare("waitpid", + ctypes.default_abi, + pid_t, + pid_t, + ctypes.int.ptr, + ctypes.int + ); + + //int kill(pid_t pid, int sig); + var kill = libc.declare("kill", + ctypes.default_abi, + ctypes.int, + pid_t, + ctypes.int + ); + + //int read(int fd, void *buf, size_t count); + var bufferSize = 1024; + var buffer = ctypes.char.array(bufferSize); + var read = libc.declare("read", + ctypes.default_abi, + ctypes.int, + ctypes.int, + buffer, + ctypes.int + ); + + //ssize_t write(int fd, const void *buf, size_t count); + var write = libc.declare("write", + ctypes.default_abi, + ctypes.int, + ctypes.int, + ctypes.char.ptr, + ctypes.int + ); + + //int chdir(const char *path); + var chdir = libc.declare("chdir", + ctypes.default_abi, + ctypes.int, + ctypes.char.ptr + ); + + //int fcntl(int fd, int cmd, ... /* arg */ ); + var fcntl = libc.declare("fcntl", + ctypes.default_abi, + ctypes.int, + ctypes.int, + ctypes.int, + ctypes.int + ); + + function popen(command, workdir, args, environment, child) { + var _in, + _out, + _err, + pid, + rc; + _in = new pipefd(); + _out = new pipefd(); + if(!options.mergeStderr) + _err = new pipefd(); + + var _args = argv(); + args.unshift(command); + for(var i=0;i= 0) { + posix_spawn_file_actions_addclose(action.address(), i); + } + } + } + + /* + * createStdinWriter () + * + * Create a ChromeWorker object for writing data to the subprocess' stdin + * pipe. The ChromeWorker object lives on a separate thread; this avoids + * internal deadlocks. + */ + function createStdinWriter() { + debugLog("Creating new stdin worker\n"); + stdinWorker = new ChromeWorker(WORKER_URL_UNIX); + stdinWorker.onmessage = function(event) { + switch (event.data.msg) { + case "info": + switch(event.data.data) { + case "WriteOK": + pendingWriteCount--; + debugLog("got OK from stdinWorker - remaining count: "+pendingWriteCount+"\n"); + break; + case "ClosedOK": + stdinOpenState = CLOSED; + debugLog("Stdin pipe closed\n"); + break; + default: + debugLog("got msg from stdinWorker: "+event.data.data+"\n"); + } + break; + case "debug": + debugLog("stdinWorker: "+event.data.data+"\n"); + break; + case "error": + LogError("got error from stdinWorker: "+event.data.data+"\n"); + pendingWriteCount = 0; + stdinOpenState = CLOSED; + } + } + stdinWorker.onerror = function(error) { + pendingWriteCount = 0; + closeStdinHandle(); + LogError("got error from stdinWorker: "+error.message+"\n"); + } + stdinWorker.postMessage({msg: "init", libc: options.libc}); + } + + /* + * writeStdin() + * @data: String containing the data to write + * + * Write data to the subprocess' stdin (equals to sending a request to the + * ChromeWorker object to write the data). + */ + function writeStdin(data) { + if (stdinOpenState == CLOSED) return; // do not write to closed pipes + + ++pendingWriteCount; + debugLog("sending "+data.length+" bytes to stdinWorker\n"); + var pipe = parseInt(child.stdin); + + stdinWorker.postMessage({ + msg: 'write', + pipe: pipe, + data: data + }); + } + + + /* + * closeStdinHandle() + * + * Close the stdin pipe, either directly or by requesting the ChromeWorker to + * close the pipe. The ChromeWorker will only close the pipe after the last write + * request process is done. + */ + + function closeStdinHandle() { + debugLog("trying to close stdin\n"); + if (stdinOpenState != OPEN) return; + stdinOpenState = CLOSEABLE; + + if (stdinWorker) { + debugLog("sending close stdin to worker\n"); + var pipePtr = parseInt(child.stdin); + + stdinWorker.postMessage({ + msg: 'close', + pipe: pipePtr + }); + } + else { + stdinOpenState = CLOSED; + debugLog("Closing Stdin\n"); + close(child.stdin) && LogError("CloseHandle stdin failed"); + } + } + + + /* + * createReader(pipe, name) + * + * @pipe: handle to the pipe + * @name: String containing the pipe name (stdout or stderr) + * @callbackFunc: function to be called with the read data + * + * Create a ChromeWorker object for reading data asynchronously from + * the pipe (i.e. on a separate thread), and passing the result back to + * the caller. + * + */ + function createReader(pipe, name, callbackFunc) { + var worker = new ChromeWorker(WORKER_URL_UNIX); + worker.onmessage = function(event) { + switch(event.data.msg) { + case "data": + debugLog("got "+event.data.count+" bytes from "+name+"\n"); + var data = ''; + if (options.charset === null) { + data = getBytes(event.data.data); + } + else { + try { + data = convertBytes(event.data.data, options.charset); + } + catch(ex) { + console.warn("error decoding output: " + ex); + data = getBytes(event.data.data); + } + } + + callbackFunc(data); + break; + case "done": + debugLog("Pipe "+name+" closed\n"); + if (event.data.data != 0) workerExitCode = event.data.data; + --readers; + if (readers == 0) cleanup(); + break; + default: + debugLog("Got msg from "+name+": "+event.data.data+"\n"); + } + } + worker.onerror = function(error) { + LogError("Got error from "+name+": "+error.message); + } + + worker.postMessage({ + msg: 'read', + pipe: pipe, + pid: pid, + libc: options.libc, + charset: options.charset === null ? "null" : options.charset, + name: name + }); + + return worker; + } + + /* + * readPipes() + * + * Open the pipes for reading from stdout and stderr + */ + function readPipes() { + + stdoutWorker = createReader(child.stdout, "stdout", function (data) { + if(options.stdout) { + setTimeout(function() { + options.stdout(data); + }, 0); + } else { + output += data; + } + }); + + if (!options.mergeStderr) stderrWorker = createReader(child.stderr, "stderr", function (data) { + if(options.stderr) { + setTimeout(function() { + options.stderr(data); + }, 0); + } else { + error += data; + } + }); + } + + function cleanup() { + debugLog("Cleanup called\n"); + if(active) { + active = false; + + closeStdinHandle(); // should only be required in case of errors + + var result, status = ctypes.int(); + result = waitpid(child.pid, status.address(), 0); + if (result > 0) + exitCode = status.value + else + if (workerExitCode >= 0) + exitCode = workerExitCode + else + exitCode = status.value; + + if (stdinWorker) + stdinWorker.postMessage({msg: 'stop'}) + + setTimeout(function _done() { + if (options.done) { + try { + options.done({ + exitCode: exitCode, + stdout: output, + stderr: error, + }); + } + catch(ex) { + // prevent from blocking if options.done() throws an error + done = true; + throw ex; + } + + } + done = true; + }, 0); + + libc.close(); + } + } + + //main + + var cmdStr = getCommandStr(options.command); + var workDir = getWorkDir(options.workdir); + + child = {}; + pid = popen(cmdStr, workDir, options.arguments, options.environment, child); + + debugLog("subprocess started; got PID "+pid+"\n"); + + readPipes(); + + if (options.stdin) { + createStdinWriter(); + if(typeof(options.stdin) == 'function') { + try { + options.stdin({ + write: function(data) { + writeStdin(data); + }, + close: function() { + closeStdinHandle(); + } + }); + } + catch(ex) { + // prevent from failing if options.stdin() throws an exception + closeStdinHandle(); + throw ex; + } + } else { + writeStdin(options.stdin); + closeStdinHandle(); + } + } + + return { + wait: function() { + // wait for async operations to complete + var thread = Cc['@mozilla.org/thread-manager;1'].getService(Ci.nsIThreadManager).currentThread; + while (! done) thread.processNextEvent(true) + return exitCode; + }, + kill: function(hardKill) { + var rv = kill(pid, (hardKill ? 9: 15)); + cleanup(-1); + return rv; + } + } +} + + +module.exports = subprocess; diff --git a/b2g/simulator/packages/subprocess/lib/subprocess_worker_unix.js b/b2g/simulator/packages/subprocess/lib/subprocess_worker_unix.js new file mode 100644 index 00000000000..a502959a5f2 --- /dev/null +++ b/b2g/simulator/packages/subprocess/lib/subprocess_worker_unix.js @@ -0,0 +1,248 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * ChromeWorker Object subprocess.jsm on Unix-like systems (Linux, Mac OS X, ...) + * to process stdin/stdout/stderr on separate threads. + * + */ + +// Being a ChromeWorker object, implicitly uses the following: +// Components.utils.import("resource://gre/modules/ctypes.jsm"); + +'use strict'; + +const BufferSize = 1024; + +var libc = null; +var libcFunc = {}; + + +/* + struct pollfd { + int fd; // file descriptor + short events; // events to look for + short revents; // events returned + }; +*/ + +var pollfd = new ctypes.StructType("pollfd", + [ {'fd': ctypes.int}, + {'events': ctypes.short}, + {'revents': ctypes.short} + ]); + +var WriteBuffer = ctypes.uint8_t.array(BufferSize); +var ReadBuffer = ctypes.char.array(BufferSize); + + +const POLLIN = 0x0001; +const POLLOUT = 0x0004; + +const POLLERR = 0x0008; // some poll error occurred +const POLLHUP = 0x0010; // file descriptor was "hung up" +const POLLNVAL = 0x0020; // requested events "invalid" + +const WNOHANG = 0x01; + +const pid_t = ctypes.int32_t; + +const INDEFINITE = -1; +const NOWAIT = 0; +const WAITTIME = 200 // wait time for poll() in ms + +function initLibc(libName) { + postMessage({msg: "debug", data: "initialising library with "+ libName}); + + libc = ctypes.open(libName); + + libcFunc.pollFds = pollfd.array(1); + + // int poll(struct pollfd fds[], nfds_t nfds, int timeout); + libcFunc.poll = libc.declare("poll", + ctypes.default_abi, + ctypes.int, + libcFunc.pollFds, + ctypes.unsigned_int, + ctypes.int); + + //ssize_t write(int fd, const void *buf, size_t count); + // NOTE: buf is declared as array of unsigned int8 instead of char to avoid + // implicit charset conversion + libcFunc.write = libc.declare("write", + ctypes.default_abi, + ctypes.int, + ctypes.int, + WriteBuffer, + ctypes.int); + + //int read(int fd, void *buf, size_t count); + libcFunc.read = libc.declare("read", + ctypes.default_abi, + ctypes.int, + ctypes.int, + ReadBuffer, + ctypes.int); + + //int pipe(int pipefd[2]); + libcFunc.pipefd = ctypes.int.array(2); + + //int close(int fd); + libcFunc.close = libc.declare("close", + ctypes.default_abi, + ctypes.int, + ctypes.int); + + //pid_t waitpid(pid_t pid, int *status, int options); + libcFunc.waitpid = libc.declare("waitpid", + ctypes.default_abi, + pid_t, + pid_t, + ctypes.int.ptr, + ctypes.int); +} + +function closePipe(pipe) { + libcFunc.close(pipe); +} + +function writePipe(pipe, data) { + + postMessage({msg: "debug", data: "trying to write to "+pipe}); + + let numChunks = Math.floor(data.length / BufferSize); + let pData = new WriteBuffer(); + + for (var chunk = 0; chunk <= numChunks; chunk ++) { + let numBytes = chunk < numChunks ? BufferSize : data.length - chunk * BufferSize; + for (var i=0; i < numBytes; i++) { + pData[i] = data.charCodeAt(chunk * BufferSize + i) % 256; + } + + let bytesWritten = libcFunc.write(pipe, pData, numBytes); + if (bytesWritten != numBytes) { + closePipe(); + libc.close(); + postMessage({ msg: "error", data: "error: wrote "+bytesWritten+" instead of "+numBytes+" bytes"}); + close(); + } + } + postMessage({msg: "info", data: "wrote "+data.length+" bytes of data"}); +} + + +function readString(data, length, charset) { + var string = '', bytes = []; + for(var i = 0;i < length; i++) { + if(data[i] == 0 && charset != "null") // stop on NULL character for non-binary data + break; + + bytes.push(data[i]); + } + + return bytes; +} + +function readPipe(pipe, charset, pid) { + var p = new libcFunc.pollFds; + p[0].fd = pipe; + p[0].events = POLLIN | POLLERR | POLLHUP; + p[0].revents = 0; + var pollTimeout = WAITTIME; + var exitCode = -1; + var readCount = 0; + var result, status = ctypes.int(); + result = 0; + + + const i=0; + while (true) { + if (result == 0) { + result = libcFunc.waitpid(pid, status.address(), WNOHANG); + if (result > 0) { + pollTimeout = NOWAIT; + exitCode = parseInt(status.value); + postMessage({msg: "debug", data: "waitpid signaled subprocess stop, exitcode="+status.value }); + } + } + var r = libcFunc.poll(p, 1, pollTimeout); + if (r > 0) { + if (p[i].revents & POLLIN) { + postMessage({msg: "debug", data: "reading next chunk"}); + readCount = readPolledFd(p[i].fd, charset); + if (readCount == 0) break; + } + + if (p[i].revents & POLLHUP) { + postMessage({msg: "debug", data: "poll returned HUP"}); + break; + } + else if (p[i].revents & POLLERR) { + postMessage({msg: "error", data: "poll returned error"}); + break; + } + else if (p[i].revents != POLLIN) { + postMessage({msg: "error", data: "poll returned "+p[i]}); + break; + } + } + else + if (pollTimeout == 0 || r < 0) break; + } + + // continue reading until the buffer is empty + while (readCount > 0) { + readCount = readPolledFd(pipe, charset); + } + + libcFunc.close(pipe); + postMessage({msg: "done", data: exitCode }); + libc.close(); + close(); +} + +function readPolledFd(pipe, charset) { + var line = new ReadBuffer(); + var r = libcFunc.read(pipe, line, BufferSize); + + if (r > 0) { + var c = readString(line, r, charset); + postMessage({msg: "data", data: c, count: c.length}); + } + return r; +} + +onmessage = function (event) { + switch (event.data.msg) { + case "init": + initLibc(event.data.libc); + break; + case "read": + initLibc(event.data.libc); + readPipe(event.data.pipe, event.data.charset, event.data.pid); + break; + case "write": + // data contents: + // msg: 'write' + // data: the data (string) to write + // pipe: ptr to pipe + writePipe(event.data.pipe, event.data.data); + postMessage({msg: "info", data: "WriteOK"}); + break; + case "close": + postMessage({msg: "debug", data: "closing stdin\n"}); + + closePipe(event.data.pipe); + postMessage({msg: "info", data: "ClosedOK"}); + break; + case "stop": + libc.close(); // do not use libc after this point + close(); + break; + default: + throw("error: Unknown command"+event.data.msg+"\n"); + } + return; +}; diff --git a/b2g/simulator/packages/subprocess/lib/subprocess_worker_win.js b/b2g/simulator/packages/subprocess/lib/subprocess_worker_win.js new file mode 100644 index 00000000000..69c3d8714bd --- /dev/null +++ b/b2g/simulator/packages/subprocess/lib/subprocess_worker_win.js @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * ChromeWorker Object subprocess.jsm on Windows to process stdin/stdout/stderr + * on separate threads. + * + */ + +// Being a ChromeWorker object, implicitly uses the following: +// Components.utils.import("resource://gre/modules/ctypes.jsm"); + +'use strict'; + +const BufferSize = 1024; + +const BOOL = ctypes.bool; +const HANDLE = ctypes.size_t; +const DWORD = ctypes.uint32_t; +const LPDWORD = DWORD.ptr; +const PVOID = ctypes.voidptr_t; +const LPVOID = PVOID; + +/* +typedef struct _OVERLAPPED { + ULONG_PTR Internal; + ULONG_PTR InternalHigh; + union { + struct { + DWORD Offset; + DWORD OffsetHigh; + }; + PVOID Pointer; + }; + HANDLE hEvent; +} OVERLAPPED, *LPOVERLAPPED; +*/ +const OVERLAPPED = new ctypes.StructType("OVERLAPPED"); + +var ReadFileBuffer = ctypes.char.array(BufferSize); +var WriteFileBuffer = ctypes.uint8_t.array(BufferSize); + +var kernel32dll = null; +var libFunc = {}; + +function initLib(libName) { + if (ctypes.size_t.size == 8) { + var WinABI = ctypes.default_abi; + } else { + var WinABI = ctypes.winapi_abi; + } + + kernel32dll = ctypes.open(libName); + + /* + BOOL WINAPI WriteFile( + __in HANDLE hFile, + __in LPCVOID lpBuffer, + __in DWORD nNumberOfBytesToWrite, + __out_opt LPDWORD lpNumberOfBytesWritten, + __inout_opt LPOVERLAPPED lpOverlapped + ); + + NOTE: lpBuffer is declared as array of unsigned int8 instead of char to avoid + implicit charset conversion + */ + libFunc.WriteFile = kernel32dll.declare("WriteFile", + WinABI, + BOOL, + HANDLE, + WriteFileBuffer, + DWORD, + LPDWORD, + OVERLAPPED.ptr + ); + + /* + BOOL WINAPI ReadFile( + __in HANDLE hFile, + __out LPVOID ReadFileBuffer, + __in DWORD nNumberOfBytesToRead, + __out_opt LPDWORD lpNumberOfBytesRead, + __inout_opt LPOVERLAPPED lpOverlapped + ); + */ + libFunc.ReadFile = kernel32dll.declare("ReadFile", + WinABI, + BOOL, + HANDLE, + ReadFileBuffer, + DWORD, + LPDWORD, + OVERLAPPED.ptr + ); + + /* + BOOL WINAPI CloseHandle( + __in HANDLE hObject + ); + */ + libFunc.CloseHandle = kernel32dll.declare("CloseHandle", + WinABI, + BOOL, + HANDLE + ); +} + + +function writePipe(pipe, data) { + var bytesWritten = DWORD(0); + + var pData = new WriteFileBuffer(); + + var numChunks = Math.floor(data.length / BufferSize); + for (var chunk = 0; chunk <= numChunks; chunk ++) { + var numBytes = chunk < numChunks ? BufferSize : data.length - chunk * BufferSize; + for (var i=0; i < numBytes; i++) { + pData[i] = data.charCodeAt(chunk * BufferSize + i) % 256; + } + + var r = libFunc.WriteFile(pipe, pData, numBytes, bytesWritten.address(), null); + if (bytesWritten.value != numBytes) + throw("error: wrote "+bytesWritten.value+" instead of "+numBytes+" bytes"); + } + postMessage("wrote "+data.length+" bytes of data"); +} + +function readString(data, length, charset) { + var string = '', bytes = []; + for(var i = 0;i < length; i++) { + if(data[i] == 0 && charset != "null") // stop on NULL character for non-binary data + break; + + bytes.push(data[i]); + } + + return bytes; +} + +function readPipe(pipe, charset) { + while (true) { + var bytesRead = DWORD(0); + var line = new ReadFileBuffer(); + var r = libFunc.ReadFile(pipe, line, BufferSize, bytesRead.address(), null); + + if (!r) { + // stop if we get an error (such as EOF reached) + postMessage({msg: "info", data: "ReadFile failed"}); + break; + } + + if (bytesRead.value > 0) { + var c = readString(line, bytesRead.value, charset); + postMessage({msg: "data", data: c, count: c.length}); + } + else { + break; + } + } + libFunc.CloseHandle(pipe); + postMessage({msg: "done"}); + kernel32dll.close(); + close(); +} + +onmessage = function (event) { + let pipePtr; + switch (event.data.msg) { + case "init": + initLib(event.data.libc); + break; + case "write": + // data contents: + // msg: 'write' + // data: the data (string) to write + // pipe: ptr to pipe + pipePtr = HANDLE.ptr(event.data.pipe); + writePipe(pipePtr.contents, event.data.data); + postMessage("WriteOK"); + break; + case "read": + initLib(event.data.libc); + pipePtr = HANDLE.ptr(event.data.pipe); + readPipe(pipePtr.contents, event.data.charset); + break; + case "close": + pipePtr = HANDLE.ptr(event.data.pipe); + postMessage("closing stdin\n"); + + if (libFunc.CloseHandle(pipePtr.contents)) { + postMessage("ClosedOK"); + } + else + postMessage("Could not close stdin handle"); + break; + case "stop": + kernel32dll.close(); + close(); + break; + default: + throw("error: Unknown command"+event.data.msg+"\n"); + } + return; +}; diff --git a/b2g/simulator/packages/subprocess/package.json b/b2g/simulator/packages/subprocess/package.json new file mode 100644 index 00000000000..f828476cefa --- /dev/null +++ b/b2g/simulator/packages/subprocess/package.json @@ -0,0 +1,15 @@ +{ + "name": "subprocess", + "license": "MPL 1.1/GPL 2.0/LGPL 2.1", + "author": "Alexandre Poirot", + "contributors": [ + "Jan Gerber (original creator) ", + "Patrick Brunschwig (author of almost all code) ", + "Ramalingam Saravanan (from enigmail team) " + ], + "version": "0.1.1", + "dependencies": [ + "api-utils" + ], + "description": "Addon-sdk package for subprocess xpcom components from enigmail. Allow to run process, manipulate stdin/out and kill it." +} diff --git a/b2g/simulator/packages/subprocess/tests/test-subprocess.js b/b2g/simulator/packages/subprocess/tests/test-subprocess.js new file mode 100644 index 00000000000..5130dd3c9b6 --- /dev/null +++ b/b2g/simulator/packages/subprocess/tests/test-subprocess.js @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const { Cc, Ci } = require("chrome"); +const subprocess = require("subprocess"); +const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + +// For now, only test on windows +if (env.get('OS') && env.get('OS').match(/Windows/)) { + +exports.testWindows = function (test) { + test.waitUntilDone(); + let envTestValue = "OK"; + let gotStdout = false; + + var p = subprocess.call({ + // Retrieve windows cmd.exe path from env + command: env.get('ComSpec'), + // In order to execute a simple "echo" function + arguments: ['/C', 'echo %ENV_TEST%'], // ' & type CON' should display stdin, but doesn't work + // Printing an environnement variable set here by the parent process + environment: ['ENV_TEST='+envTestValue], + + stdin: function(stdin) { + // Win32 command line is not really made for stdin + // So it doesn't seems to work as it's hard to retrieve stdin + stdin.write("stdin"); + stdin.close(); + }, + stdout: function(data) { + test.assert(!gotStdout,"don't get stdout twice"); + test.assertEqual(data,envTestValue+"\r\n","stdout contains the environment variable"); + gotStdout = true; + }, + stderr: function(data) { + test.fail("shouldn't get stderr"); + }, + done: function() { + test.assert(gotStdout, "got stdout before finished"); + test.done(); + }, + mergeStderr: false + }); + +} + +exports.testWindowsStderr = function (test) { + test.waitUntilDone(); + let gotStderr = false; + + var p = subprocess.call({ + command: env.get('ComSpec'), + arguments: ['/C', 'nonexistent'], + + stdout: function(data) { + test.fail("shouldn't get stdout"); + }, + stderr: function(data) { + test.assert(!gotStderr,"don't get stderr twice"); + test.assertEqual( + data, + "'nonexistent' is not recognized as an internal or external command,\r\n" + + "operable program or batch file.\r\n", + "stderr contains the error message" + ); + gotStderr = true; + }, + done: function() { + test.assert(gotStderr, "got stderr before finished"); + test.done(); + }, + mergeStderr: false + }); + +} + +} + +if (env.get('USER') && env.get('SHELL')) { + +exports.testUnix = function (test) { + test.waitUntilDone(); + let envTestValue = "OK"; + let gotStdout = false; + + var p = subprocess.call({ + command: '/bin/sh', + // Print stdin and our env variable + //arguments: ['-c', 'echo $@ $ENV_TEST'], + environment: ['ENV_TEST='+envTestValue], + + stdin: function(stdin) { + stdin.write("echo $ENV_TEST"); + stdin.close(); + }, + stdout: function(data) { + test.assert(!gotStdout,"don't get stdout twice"); + test.assertEqual(data,envTestValue+"\n","stdout contains the environment variable"); + gotStdout = true; + }, + stderr: function(data) { + test.fail("shouldn't get stderr"); + }, + done: function() { + test.assert(gotStdout, "got stdout before finished"); + test.done(); + }, + mergeStderr: false + }); +} + +exports.testUnixStderr = function (test) { + test.waitUntilDone(); + let gotStderr = false; + + var p = subprocess.call({ + // Hope that we don't have to give absolute path on linux ... + command: '/bin/sh', + arguments: ['nonexistent'], + + stdout: function(data) { + test.fail("shouldn't get stdout"); + }, + stderr: function(data) { + test.assert(!gotStderr,"don't get stderr twice"); + // There is two variant of error message + if (data == "/bin/sh: 0: Can't open nonexistent\n") + test.pass("stderr containes the expected error message"); + else + test.assertEqual(data, "/bin/sh: nonexistent: No such file or directory\n", + "stderr contains the error message"); + gotStderr = true; + }, + done: function() { + test.assert(gotStderr, "got stderr before finished"); + test.done(); + }, + mergeStderr: false + }); +} + +} diff --git a/configure.in b/configure.in index 79af440144b..37923dbf1a2 100644 --- a/configure.in +++ b/configure.in @@ -165,11 +165,20 @@ if test -z "$PERL" -o "$PERL" = ":"; then AC_MSG_ERROR([perl not found in \$PATH]) fi +if test -n "$GAIADIR" -a ! -d "$GAIADIR" ; then + AC_MSG_ERROR([GAIADIR '$GAIADIR' isn't a valid directory]) +fi + AC_SUBST(GAIADIR) if test -n "$GAIADIR" ; then AC_DEFINE(PACKAGE_GAIA) fi +if test -n "$FXOS_SIMULATOR" -a -z "$GAIADIR" ; then + AC_MSG_ERROR([FXOS_SIMULATOR=1 requires GAIADIR to be defined]) +fi +AC_SUBST(FXOS_SIMULATOR) + MOZ_ARG_WITH_STRING(gonk, [ --with-gonk=DIR location of gonk dir], @@ -4241,6 +4250,7 @@ AC_SUBST(MOZ_BUILD_APP) AC_SUBST(MOZ_PHOENIX) AC_SUBST(MOZ_XULRUNNER) AC_SUBST(MOZ_B2G) +AC_SUBST(MOZ_B2G_VERSION) AC_DEFINE_UNQUOTED(MOZ_BUILD_APP,$MOZ_BUILD_APP)