diff options
Diffstat (limited to 'toolkit/modules/subprocess/subprocess_worker_win.js')
-rw-r--r-- | toolkit/modules/subprocess/subprocess_worker_win.js | 792 |
1 files changed, 792 insertions, 0 deletions
diff --git a/toolkit/modules/subprocess/subprocess_worker_win.js b/toolkit/modules/subprocess/subprocess_worker_win.js new file mode 100644 index 0000000000..7e07361e58 --- /dev/null +++ b/toolkit/modules/subprocess/subprocess_worker_win.js @@ -0,0 +1,792 @@ +/* -*- 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); + }, +}; |