fune/toolkit/modules/subprocess/subprocess_worker_win.js
Mark Banner ef094c00c6 Bug 1797686 - Fix chrome worker ESLint definitions for toolkit/modules/subprocess. r=kmag
The current set up assumes that all files in the subprocess directory are chrome workers. This causes various globals to be assumed to be present when they are not.
Also stop using import-globals-from in preparation for migration to ES module.

Differential Revision: https://phabricator.services.mozilla.com/D160487
2022-10-27 20:17:38 +00:00

792 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";
/* eslint-env mozilla/chrome-worker */
/* 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,
polling: false,
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);
// Our poll loop is only useful if we've got at least 1 thing to poll other than our own
// signal.
if (handlers.length == 1) {
this.polling = false;
} else if (!this.polling && this.running) {
// Restart the poll loop if necessary:
setTimeout(this.loop.bind(this), 0);
this.polling = true;
}
this.eventHandlers = handlers;
let handles = handlers.map(handler => handler.event);
this.events = win32.HANDLE.array()(handles);
},
loop() {
this.poll();
if (this.running && this.polling) {
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);
},
};