mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-27 20:25:44 +00:00
820b147630
MozReview-Commit-ID: 9zr97CyY5in --HG-- extra : rebase_source : 733b47dc230a7809960d8790e88e9616544b0bc1
720 lines
18 KiB
JavaScript
720 lines
18 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
/* 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";
|
|
|
|
/* exported Process */
|
|
|
|
/* import-globals-from subprocess_shared.js */
|
|
/* import-globals-from subprocess_shared_win.js */
|
|
/* import-globals-from subprocess_worker_common.js */
|
|
importScripts("resource://gre/modules/subprocess/subprocess_shared.js",
|
|
"resource://gre/modules/subprocess/subprocess_shared_win.js",
|
|
"resource://gre/modules/subprocess/subprocess_worker_common.js");
|
|
|
|
const POLL_TIMEOUT = 5000;
|
|
|
|
// The exit code that we send when we forcibly terminate a process.
|
|
const TERMINATE_EXIT_CODE = 0x7f;
|
|
|
|
let io;
|
|
|
|
let nextPipeId = 0;
|
|
|
|
class Pipe extends BasePipe {
|
|
constructor(process, origHandle) {
|
|
super();
|
|
|
|
let handle = win32.HANDLE();
|
|
|
|
let curProc = libc.GetCurrentProcess();
|
|
libc.DuplicateHandle(curProc, origHandle, curProc, handle.address(),
|
|
0, false /* inheritable */, win32.DUPLICATE_SAME_ACCESS);
|
|
|
|
origHandle.dispose();
|
|
|
|
this.id = nextPipeId++;
|
|
this.process = process;
|
|
|
|
this.handle = win32.Handle(handle);
|
|
|
|
let event = libc.CreateEventW(null, false, false, null);
|
|
|
|
this.overlapped = win32.OVERLAPPED();
|
|
this.overlapped.hEvent = event;
|
|
|
|
this._event = win32.Handle(event);
|
|
|
|
this.buffer = null;
|
|
}
|
|
|
|
get event() {
|
|
if (this.pending.length) {
|
|
return this._event;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
maybeClose() {}
|
|
|
|
/**
|
|
* Closes the file handle.
|
|
*
|
|
* @param {boolean} [force=false]
|
|
* If true, the file handle is closed immediately. If false, the
|
|
* file handle is closed after all current pending IO operations
|
|
* have completed.
|
|
*
|
|
* @returns {Promise<void>}
|
|
* Resolves when the file handle has been closed.
|
|
*/
|
|
close(force = false) {
|
|
if (!force && this.pending.length) {
|
|
this.closing = true;
|
|
return this.closedPromise;
|
|
}
|
|
|
|
for (let {reject} of this.pending) {
|
|
let error = new Error("File closed");
|
|
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
|
|
reject(error);
|
|
}
|
|
this.pending.length = 0;
|
|
|
|
this.buffer = null;
|
|
|
|
if (!this.closed) {
|
|
this.handle.dispose();
|
|
this._event.dispose();
|
|
|
|
io.pipes.delete(this.id);
|
|
|
|
this.handle = null;
|
|
this.closed = true;
|
|
this.resolveClosed();
|
|
|
|
io.updatePollEvents();
|
|
}
|
|
return this.closedPromise;
|
|
}
|
|
|
|
/**
|
|
* Called when an error occurred while attempting an IO operation on our file
|
|
* handle.
|
|
*/
|
|
onError() {
|
|
this.close(true);
|
|
}
|
|
}
|
|
|
|
class InputPipe extends Pipe {
|
|
/**
|
|
* Queues the next chunk of data to be read from the pipe if, and only if,
|
|
* there is no IO operation currently pending.
|
|
*/
|
|
readNext() {
|
|
if (this.buffer === null) {
|
|
this.readBuffer(this.pending[0].length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the pipe if there is a pending read operation with no more
|
|
* buffered data to be read.
|
|
*/
|
|
maybeClose() {
|
|
if (this.buffer) {
|
|
let read = win32.DWORD();
|
|
|
|
let ok = libc.GetOverlappedResult(
|
|
this.handle, this.overlapped.address(),
|
|
read.address(), false);
|
|
|
|
if (!ok) {
|
|
this.onError();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously reads at most `length` bytes of binary data from the file
|
|
* descriptor into an ArrayBuffer of the same size. Returns a promise which
|
|
* resolves when the operation is complete.
|
|
*
|
|
* @param {integer} length
|
|
* The number of bytes to read.
|
|
*
|
|
* @returns {Promise<ArrayBuffer>}
|
|
*/
|
|
read(length) {
|
|
if (this.closing || this.closed) {
|
|
throw new Error("Attempt to read from closed pipe");
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.pending.push({resolve, reject, length});
|
|
this.readNext();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initializes an overlapped IO read operation to read exactly `count` bytes
|
|
* into a new ArrayBuffer, which is stored in the `buffer` property until the
|
|
* operation completes.
|
|
*
|
|
* @param {integer} count
|
|
* The number of bytes to read.
|
|
*/
|
|
readBuffer(count) {
|
|
this.buffer = new ArrayBuffer(count);
|
|
|
|
let ok = libc.ReadFile(this.handle, this.buffer, count,
|
|
null, this.overlapped.address());
|
|
|
|
if (!ok && (!this.process.handle || libc.winLastError)) {
|
|
this.onError();
|
|
} else {
|
|
io.updatePollEvents();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when our pending overlapped IO operation has completed, whether
|
|
* successfully or in failure.
|
|
*/
|
|
onReady() {
|
|
let read = win32.DWORD();
|
|
|
|
let ok = libc.GetOverlappedResult(
|
|
this.handle, this.overlapped.address(),
|
|
read.address(), false);
|
|
|
|
read = read.value;
|
|
|
|
if (!ok) {
|
|
this.onError();
|
|
} else if (read > 0) {
|
|
let buffer = this.buffer;
|
|
this.buffer = null;
|
|
|
|
let {resolve} = this.shiftPending();
|
|
|
|
if (read == buffer.byteLength) {
|
|
resolve(buffer);
|
|
} else {
|
|
resolve(ArrayBuffer.transfer(buffer, read));
|
|
}
|
|
|
|
if (this.pending.length) {
|
|
this.readNext();
|
|
} else {
|
|
io.updatePollEvents();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class OutputPipe extends Pipe {
|
|
/**
|
|
* Queues the next chunk of data to be written to the pipe if, and only if,
|
|
* there is no IO operation currently pending.
|
|
*/
|
|
writeNext() {
|
|
if (this.buffer === null) {
|
|
this.writeBuffer(this.pending[0].buffer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously writes the given buffer to our file descriptor, and returns
|
|
* a promise which resolves when the operation is complete.
|
|
*
|
|
* @param {ArrayBuffer} buffer
|
|
* The buffer to write.
|
|
*
|
|
* @returns {Promise<integer>}
|
|
* Resolves to the number of bytes written when the operation is
|
|
* complete.
|
|
*/
|
|
write(buffer) {
|
|
if (this.closing || this.closed) {
|
|
throw new Error("Attempt to write to closed pipe");
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.pending.push({resolve, reject, buffer});
|
|
this.writeNext();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initializes an overapped IO read operation to write the data in `buffer` to
|
|
* our file descriptor.
|
|
*
|
|
* @param {ArrayBuffer} buffer
|
|
* The buffer to write.
|
|
*/
|
|
writeBuffer(buffer) {
|
|
this.buffer = buffer;
|
|
|
|
let ok = libc.WriteFile(this.handle, buffer, buffer.byteLength,
|
|
null, this.overlapped.address());
|
|
|
|
if (!ok && libc.winLastError) {
|
|
this.onError();
|
|
} else {
|
|
io.updatePollEvents();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when our pending overlapped IO operation has completed, whether
|
|
* successfully or in failure.
|
|
*/
|
|
onReady() {
|
|
let written = win32.DWORD();
|
|
|
|
let ok = libc.GetOverlappedResult(
|
|
this.handle, this.overlapped.address(),
|
|
written.address(), false);
|
|
|
|
written = written.value;
|
|
|
|
if (!ok || written != this.buffer.byteLength) {
|
|
this.onError();
|
|
} else if (written > 0) {
|
|
let {resolve} = this.shiftPending();
|
|
|
|
this.buffer = null;
|
|
resolve(written);
|
|
|
|
if (this.pending.length) {
|
|
this.writeNext();
|
|
} else {
|
|
io.updatePollEvents();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class Signal {
|
|
constructor(event) {
|
|
this.event = event;
|
|
}
|
|
|
|
cleanup() {
|
|
libc.CloseHandle(this.event);
|
|
this.event = null;
|
|
}
|
|
|
|
onError() {
|
|
io.shutdown();
|
|
}
|
|
|
|
onReady() {
|
|
io.messageCount += 1;
|
|
}
|
|
}
|
|
|
|
class Process extends BaseProcess {
|
|
constructor(...args) {
|
|
super(...args);
|
|
|
|
this.killed = false;
|
|
}
|
|
|
|
/**
|
|
* Returns our process handle for use as an event in a WaitForMultipleObjects
|
|
* call.
|
|
*/
|
|
get event() {
|
|
return this.handle;
|
|
}
|
|
|
|
/**
|
|
* Forcibly terminates the process.
|
|
*/
|
|
kill() {
|
|
this.killed = true;
|
|
libc.TerminateJobObject(this.jobHandle, TERMINATE_EXIT_CODE);
|
|
}
|
|
|
|
/**
|
|
* Initializes the IO pipes for use as standard input, output, and error
|
|
* descriptors in the spawned process.
|
|
*
|
|
* @returns {win32.Handle[]}
|
|
* The array of file handles belonging to the spawned process.
|
|
*/
|
|
initPipes({stderr}) {
|
|
let our_pipes = [];
|
|
let their_pipes = [];
|
|
|
|
let secAttr = new win32.SECURITY_ATTRIBUTES();
|
|
secAttr.nLength = win32.SECURITY_ATTRIBUTES.size;
|
|
secAttr.bInheritHandle = true;
|
|
|
|
let pipe = input => {
|
|
if (input) {
|
|
let handles = win32.createPipe(secAttr, win32.FILE_FLAG_OVERLAPPED);
|
|
our_pipes.push(new InputPipe(this, handles[0]));
|
|
return handles[1];
|
|
}
|
|
let handles = win32.createPipe(secAttr, 0, win32.FILE_FLAG_OVERLAPPED);
|
|
our_pipes.push(new OutputPipe(this, handles[1]));
|
|
return handles[0];
|
|
};
|
|
|
|
their_pipes[0] = pipe(false);
|
|
their_pipes[1] = pipe(true);
|
|
|
|
if (stderr == "pipe") {
|
|
their_pipes[2] = pipe(true);
|
|
} else {
|
|
let srcHandle;
|
|
if (stderr == "stdout") {
|
|
srcHandle = their_pipes[1];
|
|
} else {
|
|
srcHandle = libc.GetStdHandle(win32.STD_ERROR_HANDLE);
|
|
}
|
|
|
|
// If we don't have a valid stderr handle, just pass it along without duplicating.
|
|
if (String(srcHandle) == win32.INVALID_HANDLE_VALUE ||
|
|
String(srcHandle) == win32.NULL_HANDLE_VALUE) {
|
|
their_pipes[2] = srcHandle;
|
|
} else {
|
|
let handle = win32.HANDLE();
|
|
|
|
let curProc = libc.GetCurrentProcess();
|
|
let ok = libc.DuplicateHandle(curProc, srcHandle, curProc, handle.address(),
|
|
0, true /* inheritable */,
|
|
win32.DUPLICATE_SAME_ACCESS);
|
|
|
|
their_pipes[2] = ok && win32.Handle(handle);
|
|
}
|
|
}
|
|
|
|
if (!their_pipes.every(handle => handle)) {
|
|
throw new Error("Failed to create pipe");
|
|
}
|
|
|
|
this.pipes = our_pipes;
|
|
|
|
return their_pipes;
|
|
}
|
|
|
|
/**
|
|
* Creates a null-separated, null-terminated string list.
|
|
*
|
|
* @param {Array<string>} strings
|
|
* @returns {win32.WCHAR.array}
|
|
*/
|
|
stringList(strings) {
|
|
// Remove empty strings, which would terminate the list early.
|
|
strings = strings.filter(string => string);
|
|
|
|
let string = strings.join("\0") + "\0\0";
|
|
|
|
return win32.WCHAR.array()(string);
|
|
}
|
|
|
|
/**
|
|
* Quotes a string for use as a single command argument, using Windows quoting
|
|
* conventions.
|
|
*
|
|
* @see https://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx
|
|
*
|
|
* @param {string} str
|
|
* The argument string to quote.
|
|
* @returns {string}
|
|
*/
|
|
quoteString(str) {
|
|
if (!/[\s"]/.test(str)) {
|
|
return str;
|
|
}
|
|
|
|
let escaped = str.replace(/(\\*)("|$)/g, (m0, m1, m2) => {
|
|
if (m2) {
|
|
m2 = `\\${m2}`;
|
|
}
|
|
return `${m1}${m1}${m2}`;
|
|
});
|
|
|
|
return `"${escaped}"`;
|
|
}
|
|
|
|
spawn(options) {
|
|
let {command, arguments: args} = options;
|
|
|
|
if (/\\cmd\.exe$/i.test(command) && args.length == 3 && /^(\/S)?\/C$/i.test(args[1])) {
|
|
// cmd.exe is insane and requires special treatment.
|
|
args = [this.quoteString(args[0]), "/S/C", `"${args[2]}"`];
|
|
} else {
|
|
args = args.map(arg => this.quoteString(arg));
|
|
}
|
|
|
|
if (/\.(bat|cmd)$/i.test(command)) {
|
|
command = io.comspec;
|
|
args = ["cmd.exe", "/s/c", `"${args.join(" ")}"`];
|
|
}
|
|
|
|
let envp = this.stringList(options.environment);
|
|
|
|
let handles = this.initPipes(options);
|
|
|
|
let processFlags = win32.CREATE_NO_WINDOW
|
|
| win32.CREATE_SUSPENDED
|
|
| win32.CREATE_UNICODE_ENVIRONMENT;
|
|
|
|
if (io.breakAwayFromJob) {
|
|
processFlags |= win32.CREATE_BREAKAWAY_FROM_JOB;
|
|
}
|
|
|
|
let startupInfoEx = new win32.STARTUPINFOEXW();
|
|
let startupInfo = startupInfoEx.StartupInfo;
|
|
|
|
startupInfo.cb = win32.STARTUPINFOW.size;
|
|
startupInfo.dwFlags = win32.STARTF_USESTDHANDLES;
|
|
|
|
startupInfo.hStdInput = handles[0];
|
|
startupInfo.hStdOutput = handles[1];
|
|
startupInfo.hStdError = handles[2];
|
|
|
|
// Note: This needs to be kept alive until we destroy the attribute list.
|
|
let handleArray = win32.HANDLE.array()(handles);
|
|
|
|
let threadAttrs = win32.createThreadAttributeList(handleArray);
|
|
if (threadAttrs) {
|
|
// If have thread attributes to pass, pass the size of the full extended
|
|
// startup info struct.
|
|
processFlags |= win32.EXTENDED_STARTUPINFO_PRESENT;
|
|
startupInfo.cb = win32.STARTUPINFOEXW.size;
|
|
|
|
startupInfoEx.lpAttributeList = threadAttrs;
|
|
}
|
|
|
|
let procInfo = new win32.PROCESS_INFORMATION();
|
|
|
|
let errorMessage = "Failed to create process";
|
|
let ok = libc.CreateProcessW(
|
|
command, args.join(" "),
|
|
null, /* Security attributes */
|
|
null, /* Thread security attributes */
|
|
true, /* Inherits handles */
|
|
processFlags, envp, options.workdir,
|
|
startupInfo.address(),
|
|
procInfo.address());
|
|
|
|
for (let handle of new Set(handles)) {
|
|
// If any of our handles are invalid, they don't have finalizers.
|
|
if (handle && handle.dispose) {
|
|
handle.dispose();
|
|
}
|
|
}
|
|
|
|
if (threadAttrs) {
|
|
libc.DeleteProcThreadAttributeList(threadAttrs);
|
|
}
|
|
|
|
if (ok) {
|
|
this.jobHandle = win32.Handle(libc.CreateJobObjectW(null, null));
|
|
|
|
let info = win32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
|
info.BasicLimitInformation.LimitFlags = win32.JOB_OBJECT_LIMIT_BREAKAWAY_OK;
|
|
|
|
ok = libc.SetInformationJobObject(this.jobHandle, win32.JobObjectExtendedLimitInformation,
|
|
ctypes.cast(info.address(), ctypes.voidptr_t),
|
|
info.constructor.size);
|
|
errorMessage = `Failed to set job limits: 0x${(ctypes.winLastError || 0).toString(16)}`;
|
|
}
|
|
|
|
if (ok) {
|
|
ok = libc.AssignProcessToJobObject(this.jobHandle, procInfo.hProcess);
|
|
if (!ok) {
|
|
errorMessage = `Failed to attach process to job object: 0x${(ctypes.winLastError || 0).toString(16)}`;
|
|
libc.TerminateProcess(procInfo.hProcess, TERMINATE_EXIT_CODE);
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
for (let pipe of this.pipes) {
|
|
pipe.close();
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
this.handle = win32.Handle(procInfo.hProcess);
|
|
this.pid = procInfo.dwProcessId;
|
|
|
|
libc.ResumeThread(procInfo.hThread);
|
|
libc.CloseHandle(procInfo.hThread);
|
|
}
|
|
|
|
/**
|
|
* Called when our process handle is signaled as active, meaning the process
|
|
* has exited.
|
|
*/
|
|
onReady() {
|
|
this.wait();
|
|
}
|
|
|
|
/**
|
|
* Attempts to wait for the process's exit status, without blocking. If
|
|
* successful, resolves the `exitPromise` to the process's exit value.
|
|
*
|
|
* @returns {integer|null}
|
|
* The process's exit status, if it has already exited.
|
|
*/
|
|
wait() {
|
|
if (this.exitCode !== null) {
|
|
return this.exitCode;
|
|
}
|
|
|
|
let status = win32.DWORD();
|
|
|
|
let ok = libc.GetExitCodeProcess(this.handle, status.address());
|
|
if (ok && status.value != win32.STILL_ACTIVE) {
|
|
let exitCode = status.value;
|
|
if (this.killed && exitCode == TERMINATE_EXIT_CODE) {
|
|
// If we forcibly terminated the process, return the force kill exit
|
|
// code that we return on other platforms.
|
|
exitCode = -9;
|
|
}
|
|
|
|
this.resolveExit(exitCode);
|
|
this.exitCode = exitCode;
|
|
|
|
this.handle.dispose();
|
|
this.handle = null;
|
|
|
|
libc.TerminateJobObject(this.jobHandle, TERMINATE_EXIT_CODE);
|
|
this.jobHandle.dispose();
|
|
this.jobHandle = null;
|
|
|
|
for (let pipe of this.pipes) {
|
|
pipe.maybeClose();
|
|
}
|
|
|
|
io.updatePollEvents();
|
|
|
|
return exitCode;
|
|
}
|
|
}
|
|
}
|
|
|
|
io = {
|
|
events: null,
|
|
eventHandlers: null,
|
|
|
|
pipes: new Map(),
|
|
|
|
processes: new Map(),
|
|
|
|
messageCount: 0,
|
|
|
|
running: true,
|
|
|
|
init(details) {
|
|
this.comspec = details.comspec;
|
|
|
|
let signalEvent = ctypes.cast(ctypes.uintptr_t(details.signalEvent),
|
|
win32.HANDLE);
|
|
this.signal = new Signal(signalEvent);
|
|
this.updatePollEvents();
|
|
|
|
this.breakAwayFromJob = details.breakAwayFromJob;
|
|
|
|
setTimeout(this.loop.bind(this), 0);
|
|
},
|
|
|
|
shutdown() {
|
|
if (this.running) {
|
|
this.running = false;
|
|
|
|
this.signal.cleanup();
|
|
this.signal = null;
|
|
|
|
self.postMessage({msg: "close"});
|
|
self.close();
|
|
}
|
|
},
|
|
|
|
getPipe(pipeId) {
|
|
let pipe = this.pipes.get(pipeId);
|
|
|
|
if (!pipe) {
|
|
let error = new Error("File closed");
|
|
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
|
|
throw error;
|
|
}
|
|
return pipe;
|
|
},
|
|
|
|
getProcess(processId) {
|
|
let process = this.processes.get(processId);
|
|
|
|
if (!process) {
|
|
throw new Error(`Invalid process ID: ${processId}`);
|
|
}
|
|
return process;
|
|
},
|
|
|
|
updatePollEvents() {
|
|
let handlers = [this.signal,
|
|
...this.pipes.values(),
|
|
...this.processes.values()];
|
|
|
|
handlers = handlers.filter(handler => handler.event);
|
|
|
|
this.eventHandlers = handlers;
|
|
|
|
let handles = handlers.map(handler => handler.event);
|
|
this.events = win32.HANDLE.array()(handles);
|
|
},
|
|
|
|
loop() {
|
|
this.poll();
|
|
if (this.running) {
|
|
setTimeout(this.loop.bind(this), 0);
|
|
}
|
|
},
|
|
|
|
|
|
poll() {
|
|
let timeout = this.messageCount > 0 ? 0 : POLL_TIMEOUT;
|
|
for (;; timeout = 0) {
|
|
let events = this.events;
|
|
let handlers = this.eventHandlers;
|
|
|
|
let result = libc.WaitForMultipleObjects(events.length, events,
|
|
false, timeout);
|
|
|
|
if (result < handlers.length) {
|
|
try {
|
|
handlers[result].onReady();
|
|
} catch (e) {
|
|
console.error(e);
|
|
debug(`Worker error: ${e} :: ${e.stack}`);
|
|
handlers[result].onError();
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
addProcess(process) {
|
|
this.processes.set(process.id, process);
|
|
|
|
for (let pipe of process.pipes) {
|
|
this.pipes.set(pipe.id, pipe);
|
|
}
|
|
},
|
|
|
|
cleanupProcess(process) {
|
|
this.processes.delete(process.id);
|
|
},
|
|
};
|