Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
// -*- coding: utf-8 -*-
// vim: et:ts=4:sw=4:sts=4:ft=javascript
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public
* License Version 1.1 (the "MPL"); you may not use this file
* except in compliance with the MPL. You may obtain a copy of
* the MPL at http://www.mozilla.org/MPL/
*
* Software distributed under the MPL is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the MPL for the specific language governing
* rights and limitations under the MPL.
*
* The Original Code is subprocess.jsm.
*
* The Initial Developer of this code is Jan Gerber.
* Portions created by Jan Gerber <j@mailb.org>
* are Copyright (C) 2011 Jan Gerber.
* All Rights Reserved.
*
* Contributor(s):
* Patrick Brunschwig <patrick@enigmail.net>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
* ***** END LICENSE BLOCK ***** */
/*
* Import into a JS component using
* 'Components.utils.import("resource://firefogg/subprocess.jsm");'
*
* This object allows to start a process, and read/write data to/from it
* using stdin/stdout/stderr streams.
* Usage example:
*
* 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 <command>, 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 explicitly 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. Default is false.
*
* bufferedOutput: optional boolean value. If true, stderr and stdout are buffered
* and will only deliver data when a certain amount of output is
* available. Enabling the option will give you some performance
* benefits if your read a lot of data. Don't enable this if your
* application works in a conversation-like mode. Default is false.
*
*
* 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); } );
*/
'use strict';
Components.utils.import("resource://gre/modules/ctypes.jsm");
this.EXPORTED_SYMBOLS = ['subprocess'];
const Cc = Components.classes;
const Ci = Components.interfaces;
const NS_LOCAL_FILE = "@mozilla.org/file/local;1";
//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 ],
'android': [ 'libc.so', 2024 , ctypes.unsigned_long, 7 ]
};
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 convertBytes(data, charset) {
var string = '';
charset = charset || 'UTF-8';
var unicodeConv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.getService(Ci.nsIScriptableUnicodeConverter);
try {
unicodeConv.charset = charset;
string = unicodeConv.ConvertToUnicode(data);
} catch (ex) {
LogError("String conversion failed: "+ex.toString()+"\n");
string = '';
}
string += unicodeConv.Finish();
return string;
}
function getCommandStr(command) {
let commandStr = null;
if (typeof(command) == "string") {
let file = Cc[NS_LOCAL_FILE].createInstance(Ci.nsIFile);
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(Ci.nsIFile);
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 = this.subprocess = {
call: function(options) {
options.mergeStderr = options.mergeStderr || false;
options.bufferedOutput = options.bufferedOutput || false;
options.workdir = options.workdir || null;
options.environment = options.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.toLowerCase() == "android") {
return subprocess_android(options);
}
else 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;
},
getPlatformValue: getPlatformValue
};
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 = 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');
// 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 (!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 (!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 = 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("subprocess_worker_win.js");
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--;
exitCode = -2;
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("subprocess_worker_win.js");
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 = event.data.data;
}
else
data = convertBytes(event.data.data, options.charset);
callbackFunc(data);
break;
case "done":
debugLog("Pipe "+name+" closed\n");
--readers;
if (readers == 0) cleanup();
break;
case "error":
exitCode = -2;
LogError("Got msg from "+name+": "+event.data.data+"\n");
break;
default:
debugLog("Got msg from "+name+": "+event.data.data+"\n");
}
};
worker.onerror = function(errorMsg) {
LogError("Got error from "+name+": "+errorMsg.message);
exitCode = -2;
};
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,
bufferedOutput: options.bufferedOutput,
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;
}
});
stderrWorker = createReader(child.stderr, "stderr", function (data) {
var output = options.mergeStderr ? 'stdout' : 'stderr';
if(options[output]) {
setTimeout(function() {
options[output](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());
if (exitCode > -2)
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;
}
};
}
/**
* Limited varation of the Linux version that works on Android / B2G.
* It does not do stdin, stderr or workdir.
* But it can execute stuff so there's that.
*/
function subprocess_android(options) {
function createAndroidReader(pipe, dataFunc, doneFunc) {
var name = 'stdout';
var worker = new ChromeWorker("subprocess_worker_unix.js");
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 = event.data.data;
}
else
data = convertBytes(event.data.data, options.charset);
dataFunc(data);
break;
case "done":
debugLog("Pipe "+name+" closed\n");
doneFunc(event.data.data);
break;
case "error":
LogError("Got error from "+name+": "+event.data.data);
exitCode = -2;
break;
default:
debugLog("Got msg from "+name+": "+event.data.data+"\n");
}
};
worker.onerror = function(error) {
LogError("Got error from "+name+": "+error.message);
exitCode = -2;
};
worker.postMessage({
msg: 'read',
pipe: pipe,
pid: -1,
libc: options.libc,
charset: options.charset === null ? "null" : options.charset,
bufferedOutput: options.bufferedOutput,
name: name
});
return worker;
}
var libc = ctypes.open(options.libc);
const FILE = new ctypes.StructType("FILE").ptr;
var libc = ctypes.open(options.libc);
// FILE *popen(const char *command, const char *type);
var popen = libc.declare("popen",
ctypes.default_abi,
FILE,
ctypes.char.ptr,
ctypes.char.ptr);
// int *pclose(FILE *stream);
var pclose = libc.declare("pclose",
ctypes.default_abi,
ctypes.int,
FILE);
// int fileno(FILE *stream).
var fileno = libc.declare("fileno",
ctypes.default_abi,
ctypes.int,
FILE);
var fileptr = popen([ options.command ].concat(options.arguments || []).join(' '), 'r');
var fd = fileno(fileptr);
let outdata = '';
createAndroidReader(fd, function(data) {
outdata += data;
if (options.stdout) {
options.stdout(data);
}
}, function(exitCode) {
pclose(fileptr);
options.done({
exitCode: exitCode,
stdout: outdata,
stderr: ''
});
});
}
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 = 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.
// At 2013/10/28, its size was 80 on linux, but better be safe (and larger),
// than crash when posix_spawn_file_actions_init fill `action` with zeros.
// Use `gcc sizeof_fileaction.c && ./a.out` to check that size.
var posix_spawn_file_actions_t = ctypes.uint8_t.array(100);
//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,
i;
_in = new pipefd();
_out = new pipefd();
_err = new pipefd();
var _args = argv();
args.unshift(command);
for(i=0;i<args.length;i++) {
_args[i] = ctypes.char.array()(args[i]);
}
var _envp = envp();
for(i=0;i<environment.length;i++) {
_envp[i] = ctypes.char.array()(environment[i]);
}
rc = pipe(_in);
if (rc < 0) {
return -1;
}
rc = pipe(_out);
fcntl(_out[0], F_SETFL, getPlatformValue(O_NONBLOCK));
if (rc < 0) {
close(_in[0]);
close(_in[1]);
return -1;
}
rc = pipe(_err);
fcntl(_err[0], F_SETFL, getPlatformValue(O_NONBLOCK));
if (rc < 0) {
close(_in[0]);
close(_in[1]);
close(_out[0]);
close(_out[1]);
return -1;
}
let STDIN_FILENO = 0;
let STDOUT_FILENO = 1;
let STDERR_FILENO = 2;
let action = posix_spawn_file_actions_t();
posix_spawn_file_actions_init(action.address());
posix_spawn_file_actions_adddup2(action.address(), _in[0], STDIN_FILENO);
posix_spawn_file_actions_addclose(action.address(), _in[1]);
posix_spawn_file_actions_addclose(action.address(), _in[0]);
posix_spawn_file_actions_adddup2(action.address(), _out[1], STDOUT_FILENO);
posix_spawn_file_actions_addclose(action.address(), _out[1]);
posix_spawn_file_actions_addclose(action.address(), _out[0]);
posix_spawn_file_actions_adddup2(action.address(), _err[1], STDERR_FILENO);
posix_spawn_file_actions_addclose(action.address(), _err[1]);
posix_spawn_file_actions_addclose(action.address(), _err[0]);
// posix_spawn doesn't support setting a custom workdir for the child,
// so change the cwd in the parent process before launching the child process.
if (workdir) {
if (chdir(workdir) < 0) {
throw new Error("Unable to change workdir before launching child process");
}
}
closeOtherFds(action, _in[1], _out[0], _err[0]);
let id = pid_t(0);
let rv = posix_spawn(id.address(), command, action.address(), null, _args, _envp);
posix_spawn_file_actions_destroy(action.address());
if (rv != 0) {
// we should not really end up here
close(_err[0]);
close(_err[1]);
close(_out[0]);
close(_out[1]);
close(_in[0]);
close(_in[1]);
throw new Error("Fatal - failed to create subprocess '"+command+"'");
}
pid = id.value;
close(_in[0]);
close(_out[1]);
close(_err[1]);
child.stdin = _in[1];
child.stdout = _out[0];
child.stderr = _err[0];
child.pid = pid;
return pid;
}
// close any file descriptors that are not required for the process
function closeOtherFds(action, fdIn, fdOut, fdErr) {
// Unfortunately on mac, any fd registered in posix_spawn_file_actions_addclose
// that can't be closed correctly will make posix_spawn fail...
// Even if we ensure registering only still opened fds.
if (gXulRuntime.OS == "Darwin")
return;
var maxFD = 256; // arbitrary max
var rlim_t = getPlatformValue(RLIM_T);
const RLIMITS = new ctypes.StructType("RLIMITS", [
{"rlim_cur": rlim_t},
{"rlim_max": rlim_t}
]);
try {
var getrlimit = libc.declare("getrlimit",
ctypes.default_abi,
ctypes.int,
ctypes.int,
RLIMITS.ptr
);
var rl = new RLIMITS();
if (getrlimit(getPlatformValue(RLIMIT_NOFILE), rl.address()) == 0) {
if (rl.rlim_cur < Math.pow(2,20)) // ignore too high numbers
maxFD = rl.rlim_cur;
}
debugLog("getlimit: maxFD="+maxFD+"\n");
}
catch(ex) {
debugLog("getrlimit: no such function on this OS\n");
debugLog(ex.toString());
}
// close any file descriptors
// fd's 0-2 are already closed
for (var i = 3; i < maxFD; i++) {
if (i != fdIn && i != fdOut && i != fdErr && fcntl(i, F_GETFD, -1) >= 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("subprocess_worker_unix.js");
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;
exitCode = -2;
}
};
stdinWorker.onerror = function(error) {
pendingWriteCount = 0;
exitCode = -2;
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("subprocess_worker_unix.js");
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 = event.data.data;
}
else
data = convertBytes(event.data.data, options.charset);
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;
case "error":
LogError("Got error from "+name+": "+event.data.data);
exitCode = -2;
break;
default:
debugLog("Got msg from "+name+": "+event.data.data+"\n");
}
};
worker.onerror = function(error) {
LogError("Got error from "+name+": "+error.message);
exitCode = -2;
};
worker.postMessage({
msg: 'read',
pipe: pipe,
pid: pid,
libc: options.libc,
charset: options.charset === null ? "null" : options.charset,
bufferedOutput: options.bufferedOutput,
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;
}
});
stderrWorker = createReader(child.stderr, "stderr", function (data) {
var output = options.mergeStderr ? 'stdout' : 'stderr';
if(options[output]) {
setTimeout(function() {
options[output](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 (exitCode > -2) {
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();
}
}
else
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;
}
};
}