UNPKG

gpii-windows

Version:

Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™

621 lines (545 loc) 24.4 kB
/* * Windows Process Handling. * A wrapper to integrate processHandlingCore into GPII. * * Copyright 2016 Raising the Floor - International * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * * The research leading to these results has received funding from the European Union's * Seventh Framework Programme (FP7/2007-2013) * under grant agreement no. 289016. * * You may obtain a copy of the License at * https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; var fluid = require("gpii-universal"); var ref = require("ref"), child_process = require("child_process"), path = require("path"); var gpii = fluid.registerNamespace("gpii"); var windows = fluid.registerNamespace("gpii.windows"); require("../WindowsUtilities/WindowsUtilities.js"); require("../processReporter/processReporter.js"); require("../registrySettingsHandler"); var c = windows.API_constants; var processesBridge = gpii.processes.windows(); /** * Kills any windows processes with a given application filename. * http://stackoverflow.com/questions/7956519/how-to-kill-processes-by-name-win32-api * * @param {String} filename The filename of the application. For example, the Windows on * screen keyboard is "osk.exe". Other examples include "Magnify.exe" and * "firefox.exe". */ gpii.windows.killProcessByName = function (filename) { var procs = processesBridge.findProcessesByCommand(filename); for (var n = 0, len = procs.length; n < len; n++) { var hProcess = windows.kernel32.OpenProcess(c.PROCESS_TERMINATE, 0, procs[n].pid); if (hProcess) { windows.kernel32.TerminateProcess(hProcess, 9); windows.kernel32.CloseHandle(hProcess); } } }; /** * Gets the full path of a running process. * * If the process is running as another user (or the system), then getProcessExe is called, where only the filename of * the executable is available. * * Originally, the process path was taken from the process reporter. However, it was too slow to be called from * the metrics (when it records the active window) because it was blocking the UI. Work can be done to improve the * process reporter's getProcessPath (or even hide it in another thread), but it still wouldn't be as quick as this * method due to it capturing several pieces of information about every process. GPII-3471. * * @param {Number} pid The process id. * @return {String} The executable file, including the full path if available. */ gpii.windows.getProcessPath = function (pid) { var processPath = null; var hProcess = windows.kernel32.OpenProcess(c.PROCESS_QUERY_INFORMATION, 0, pid); if (hProcess) { try { var maxLength = 0x400; var pathBuffer = Buffer.alloc(maxLength); var length = ref.alloc(windows.types.DWORD, maxLength); var ret = windows.kernel32.QueryFullProcessImageNameW(hProcess, 0, pathBuffer, length); if (ret) { processPath = gpii.windows.stringFromWideChar(pathBuffer); } else { fluid.log(windows.win32errorText("QueryFullProcessImageName", ret)); } } finally { windows.kernel32.CloseHandle(hProcess); } } if (!processPath) { processPath = gpii.windows.getProcessExe(pid); } return processPath; }; /** * Gets the executable file of a running process. * getProcessPath calls this when it is unable to get the full path of a process. The method used in this function * can be used against processes by other users, but it does not return the full path. * * @param {Number} pid The process id. * @return {String} The filename of the executable. */ gpii.windows.getProcessExe = function (pid) { var processExe = null; // Take the process snapshot. var snapShot = windows.kernel32.CreateToolhelp32Snapshot(windows.API_constants.TH32CS_SNAPPROCESS, null); if (snapShot === windows.API_constants.INVALID_HANDLE_VALUE) { fluid.log(windows.win32errorText("CreateToolhelp32Snapshot", snapShot)); } else { try { var processEntry = new windows.PROCESSENTRY32(); processEntry.dwSize = windows.PROCESSENTRY32.size; // Enumerate the processes var gotProcess = windows.kernel32.Process32First(snapShot, processEntry.ref()); while (gotProcess) { if (processEntry.th32ProcessID === pid) { // The matching process. processExe = ref.readCString(processEntry.szExeFile.buffer); break; } // Get the next one gotProcess = windows.kernel32.Process32Next(snapShot, processEntry.ref()); } } finally { windows.kernel32.CloseHandle(snapShot); } } return processExe; }; /** * Determines if a given process is running, returning true if it is. * * @param {String|Number} proc The name of the executable, or the process ID. * @return {Boolean} true if the process is running. */ gpii.windows.isProcessRunning = function (proc) { var togo = false; if (isNaN(proc)) { togo = gpii.processReporter.find(proc); } else { try { process.kill(proc, 0); togo = true; } catch (e) { togo = false; } } return togo; }; /** * Waits for a process to either start or terminate, returning a promise which will resolve when the existence of a * process is in the desired state. * * The promise will reject if the process has yet to become in the desired state after the timeout. * * @param {String|Number} proc The name of the executable, or the process ID. * @param {Object} options The options. * @param {Boolean} options.start The desired state: true to wait for the process to start, false to wait for the termination. * @param {Number} options.timeout Approximate milliseconds to wait, or null for infinite. * @param {Number} options.pollDelay How long to wait, in milliseconds, between checking for the process. * @param {Boolean} options.dontReject If true, then resolve the promise with "timeout" instead of rejecting. * @return {Promise} The promise will resolve when the process is in the desired state, or will reject if after the * timeout the process is still in the same state. */ gpii.windows.waitForProcessState = function (proc, options) { var defaultOptions = { pollDelay: 500, timeout: null, start: false }; options = fluid.extend(true, defaultOptions, options); var waitOptions = { argument: proc, conditionValue: options.start, pollDelay: options.pollDelay, timeout: options.timeout, dontReject: options.dontReject, error: { isError: true, message: "Timed out waiting for process " + proc + " to " + (options.start ? "start" : "terminate") + " after " + options.timeout + "ms" } }; return gpii.windows.waitForCondition(gpii.windows.isProcessRunning, waitOptions); }; /** * Waits for a process to terminate, returning a promise which will resolve when there are no matching processes * running. If there are no processes running when the function is called, the promise will already be resolved. * The promise will reject if the process is still running after the timeout. * * @param {String|Number} proc The name of the executable, or the process ID. * @param {Object} userOptions The options. * @param {Number} userOptions.timeout Approximate milliseconds to wait, or null for infinite. * @param {Number} userOptions.pollDelay How long to wait, in milliseconds, between checking for the process. * @param {Boolean} userOptions.dontReject If true, then resolve the promise with "timeout" instead of rejecting. * @return {Promise} The promise will resolve when there are no matching processes running, or will reject if a matching * process is still running after the timeout. */ gpii.windows.waitForProcessTermination = function (proc, userOptions) { var options = fluid.extend(true, {start: false}, userOptions); return gpii.windows.waitForProcessState(proc, options); }; /** * Waits for a process to start, returning a promise which will resolve when there there is a matching process running. * If there are already processes running when the function is called, the promise will already be resolved. * The promise will reject if a matching process is still not running after the timeout. * * @param {String|Number} proc The name of the executable, or the process ID. * @param {Object} userOptions The options. * @param {Number} userOptions.timeout Approximate milliseconds to wait, or null for infinite. * @param {Number} userOptions.pollDelay How long to wait, in milliseconds, between checking for the process. * @param {Boolean} userOptions.dontReject If true, then resolve the promise with "timeout" instead of rejecting. * @return {Promise} The promise will resolve when there is a matching processes running, or will reject if a matching * process is still not running after the timeout. */ gpii.windows.waitForProcessStart = function (proc, userOptions) { var options = fluid.extend(true, {start: true}, userOptions); return gpii.windows.waitForProcessState(proc, options); }; /** * Terminates a process in a kind manner by sending WM_QUIT to the windows it owns. This should allow the process to * perform any clean up tasks. * * If options.gracefulOnly is false (default), and if the process has not shutdown after the timeout or the process * does not have any Windows, then the process will be terminated. * * The returned promise resolves when the process has ended, with a boolean indicating whether a clean shutdown was * possible. * * @param {String} filename The filename of the process. * @param {Object} options The options. * @param {Number} options.timeout How long to wait for the process to die, in milliseconds. * @param {Boolean} options.cleanOnly true to reject if the process can't be closed cleanly; false to force the termination. * @param {Number} options.exitCode The exit code the application should return. * @return {Promise} Resolves when the process has ended, with a boolean indicating whether a clean shutdown was possible. */ gpii.windows.closeProcessByName = function (filename, options) { var defaultOptions = { timeout: 15000, cleanOnly: false, exitCode: 0 }; options = fluid.extend(defaultOptions, options); var pids = fluid.transform(processesBridge.findProcessesByCommand(filename), function (proc) { return proc.pid; }); if (!pids) { // Process is not running. return fluid.toPromise(true); } var foundWindow = false; // Enumerate all the top-level windows on the desktop, to see which ones are owned by the process. gpii.windows.enumerateWindows(function (hwnd) { // Get the process ID that owns the Window. var ptr = ref.alloc(windows.types.DWORD); gpii.windows.user32.GetWindowThreadProcessId(hwnd, ptr); var windowPid = ptr.deref(); if (pids.indexOf(windowPid) !== -1) { if (options.closeWindow) { // Send WM_CLOSE, which tells the application to exit nicely. gpii.windows.user32.PostMessageW(hwnd, windows.API_constants.WM_CLOSE, options.exitCode, 0); } else { // Send WM_QUIT, which tells the thread to terminate. gpii.windows.user32.PostMessageW(hwnd, windows.API_constants.WM_QUIT, options.exitCode, 0); } foundWindow = true; } }); var promiseTogo = fluid.promise(); if (foundWindow) { // Wait for the process to die. gpii.windows.waitForProcessTermination(filename, { timeout: options.timeout }) .then(function () { promiseTogo.resolve(true); }, function (err) { // Taking too long to die. if (options.cleanOnly) { promiseTogo.reject({ isError: true, message: "Process " + filename + " will not close cleanly: " + err.message, error: err }); } else { // Force it to terminate gpii.windows.killProcessByName(filename); // This promise was rejected but the return promise should resolve as the process is terminated. promiseTogo.resolve(false); } }); } else { // The process does not have any windows so it can't be shutdown cleanly. if (options.cleanOnly) { promiseTogo.reject({ isError: true, message: "Process " + filename + " will not close cleanly: No windows belong to the process" }); } else { // Force it to terminate. gpii.windows.killProcessByName(filename); promiseTogo.resolve(false); } } return promiseTogo; }; /** * Determines the current state of a service, which can be one of paused, running, or stopped: * * * @param {String} serviceName The service name (The "service name", not the "display name"). * @return {String} paused, running, stopped, or unknown. */ gpii.windows.getServiceState = function (serviceName) { var managerHandle = gpii.windows.advapi32.OpenSCManagerW(0, 0, gpii.windows.API_constants.SC_MANAGER_CONNECT); var serviceHandle; if (managerHandle) { var serviceBuffer = gpii.windows.stringToWideChar(serviceName); serviceHandle = gpii.windows.advapi32.OpenServiceW(managerHandle, serviceBuffer, gpii.windows.API_constants.SERVICE_QUERY_STATUS); } var stateNumber; if (serviceHandle) { var serviceStatus = new gpii.windows.SERVICE_STATUS(); var success = gpii.windows.advapi32.QueryServiceStatus(serviceHandle, serviceStatus.ref()); if (success) { stateNumber = serviceStatus.dwCurrentState; } gpii.windows.advapi32.CloseServiceHandle(serviceHandle); } if (managerHandle) { gpii.windows.advapi32.CloseServiceHandle(managerHandle); } var state; switch (stateNumber) { case gpii.windows.API_constants.SERVICE_PAUSE_PENDING: case gpii.windows.API_constants.SERVICE_PAUSED: state = "paused"; break; case gpii.windows.API_constants.SERVICE_CONTINUE_PENDING: case gpii.windows.API_constants.SERVICE_START_PENDING: case gpii.windows.API_constants.SERVICE_RUNNING: state = "running"; break; case gpii.windows.API_constants.SERVICE_STOP_PENDING: case gpii.windows.API_constants.SERVICE_STOPPED: state = "stopped"; break; default: state = "unknown"; } return state; }; /** * Gets the handle of the tasktray window. * @return {Number} The tasktray window handle, 0 if not found. */ gpii.windows.getTasktrayWindow = function () { var classname = windows.stringToWideChar("Shell_TrayWnd"); return windows.user32.FindWindowW(classname, ref.NULL); }; /** * Gets the PID of the explorer process that owns the tasktray. * @return {Number} The process ID of explorer. */ gpii.windows.getExplorerProcess = function () { var trayWindow = windows.getTasktrayWindow(); return trayWindow && windows.getWindowProcessId(trayWindow); }; /** * Stops the Windows Explorer shell process, including the folder windows. * * Use restartExplorer() to restart it. * * @param {Object} options [optional] Options object. * @param {Number} options.trayWindow Override the task tray's window handle. * @param {Boolean} options.ignoreFolders True to keep the folder windows open [default: false]. * @param {Number} options.timeout Timeout, in ms, to wait before forcefully terminating the process [default: 8000]. * @param {String} options.explorerExe The explorer executable name to kill [default: explorer.exe] * @return {Promise} Resolves when explorer has stopped. */ gpii.windows.stopExplorer = function (options) { options = Object.assign({ trayWindow: null, ignoreFolders: false, timeout: 8000, explorerExe: "explorer.exe" }, options); var promise = fluid.promise(); fluid.log("Stopping explorer"); if (gpii.windows.isProcessRunning(options.explorerExe)) { // The tasktray handles a window message of 0x5b4 which causes it to save state and shutdown. // See https://stackoverflow.com/questions/5689904/gracefully-exit-explorer-programmatically var closeExplorerMessage = gpii.windows.API_constants.WM_USER + 0x1b4; var trayWindow = options.trayWindow || windows.getTasktrayWindow(); // The folder windows also need to be closed, otherwise future ones will have a mixture of languages. var explorerWindows = options.ignoreFolders ? [] : windows.findWindows("CabinetWClass"); var closeTrayPromise; if (trayWindow) { windows.user32.PostMessageW(trayWindow, closeExplorerMessage, 0, 0); // Unable to completely make explorer terminate without killing it, so WM_ENDSESSION is being sent so explorer // should at least expect to be killed. windows.user32.PostMessageW(trayWindow, windows.API_constants.WM_QUERYENDSESSION, 0, 1); closeTrayPromise = gpii.windows.messages.sendMessageAsync(trayWindow, windows.API_constants.WM_ENDSESSION, 1, 1, options.timeout); } else { closeTrayPromise = fluid.promise().resolve(); } // Also instruct the folder windows to shutdown. Sending WM_QUERYENDSESSION causes the windows to re-appear when // explorer is restarted. fluid.each(explorerWindows, function (window) { windows.user32.PostMessageW(window, windows.API_constants.WM_QUERYENDSESSION, 0, 1); windows.user32.PostMessageW(window, windows.API_constants.WM_ENDSESSION, 1, 1); }); // Wait for the tray to process the shutdown message. closeTrayPromise.then(function (value) { if (value === "timeout") { fluid.log("Explorer did not respond to shutdown request"); } else { fluid.log("Explorer processed shutdown"); // Explorer now expects to be killed windows.killProcessByName(options.explorerExe); } // The process has been known to linger, preventing a new instance. Give it until the timeout, then be more forceful windows.waitForProcessTermination(options.explorerExe, {timeout: options.timeout}).then(promise.resolve, function () { fluid.log("Explorer did not close - terminating"); windows.killProcessByName(options.explorerExe); promise.resolve(); }); }); } else { // Explorer was not running. promise.resolve(); } return promise; }; /** * Restarts the Windows Explorer shell process. * * stopExplorer will be called if it hasn't been called already. * * @param {Object} options Options. * @param {String} options.explorer Path to the explorer executable [default: %SystemRoot%\explorer.exe] * @param {String} options.explorerExe Executable name of explorer [default: taken from options.explorer] * @param {Number} options.trayWindow Override the task tray's window handle. * @param {Number} options.timeout Timeout, in ms, to wait before forcefully terminating the process. * @param {Array<String>} options.args Arguments to pass to explorer * @return {Promise} Resolves when explorer has restarted. */ gpii.windows.restartExplorer = function (options) { options = Object.assign({}, options); if (!options.explorer) { options.explorer = process.env.SystemRoot + "\\explorer.exe"; options.args = ["/LOADSAVEDWINDOWS"]; } if (!options.explorerExe) { options.explorerExe = path.basename(options.explorer); } // Hide the taskbar, in case someone tries to interact with it just before it restarts. var trayWindow = options.trayWindow || windows.getTasktrayWindow(); if (trayWindow) { gpii.windows.user32.ShowWindow(trayWindow, 0); } var work = [ function () { // Don't restart explorer while high-contrast is currently being applied. var p = fluid.promise(); gpii.windows.waitForProcessStart("sethc.exe", {timeout: 1000}).then(function () { fluid.promise.follow(gpii.windows.waitForProcessTermination("sethc.exe", {timeout: 5000}), p); }, p.resolve); return p; }, gpii.windows.stopExplorer, function () { // Wait for explorer to completely close. return windows.waitForProcessTermination(options.explorerExe, {timeout: 1000, dontReject: true}); }, function () { fluid.log("Starting explorer"); var child = child_process.spawn(options.explorer, options.args, { detached: true, stdio: "ignore" }); child.on("error", function (err) { fluid.log("Error starting explorer:", err); }); return windows.waitForCondition(windows.getTasktrayWindow, {timeout: options.timeout}); } ]; return fluid.promise.sequence(work, options); }; /** * Starts a process. * * Use this in favour of built-in child_process functions only if controlling the initial window is desired. * * Not all processes will honour the initial window state value. * * @param {String} executable The executable path. * @param {String|Array<String>} args The arguments (passed directly to the process, with no shell expansion). * @param {Object} options [optional] Options * @param {String} options.windowState The state of the process's first window. "hide", "normal", "minimized", * "maximized", or "noactivate". * * @return {Number} The pid of the new process. */ gpii.windows.startProcess = function (executable, args, options) { var argArray = fluid.makeArray(args); // Quote the executable name, in case it contains spaces argArray.unshift("\"" + executable + "\""); var commandLine = windows.stringToWideChar(argArray.join(" ")); var showWindow = ({ hide: gpii.windows.API_constants.SW_HIDE, normal: gpii.windows.API_constants.SW_SHOWNORMAL, minimized: gpii.windows.API_constants.SW_SHOWMINIMIZED, maximized: gpii.windows.API_constants.SW_SHOWMAXIMIZED, noactivate: gpii.windows.API_constants.SW_SHOWNOACTIVATE })[options && options.windowState]; var startupInfo = new windows.STARTUPINFO(); startupInfo.ref().fill(0); startupInfo.cb = windows.STARTUPINFO.size; if (showWindow !== undefined) { startupInfo.dwFlags |= windows.API_constants.STARTF_USESHOWWINDOW; startupInfo.wShowWindow = showWindow; } var processInfoBuf = new windows.PROCESS_INFORMATION(); processInfoBuf.ref().fill(0); var ret = windows.kernel32.CreateProcessW(ref.NULL, commandLine, ref.NULL, ref.NULL, 0, 0, ref.NULL, ref.NULL, startupInfo.ref(), processInfoBuf.ref()); if (!ret) { var errorText = windows.win32errorText("CreateProcessW", ret); fluid.log("Trouble starting '" + argArray.join(" ") + "': ", errorText); } windows.kernel32.CloseHandle(processInfoBuf.hThread); windows.kernel32.CloseHandle(processInfoBuf.hProcess); return ret ? processInfoBuf.dwProcessId : 0; }; fluid.defaults("gpii.windows.stopExplorer", { gradeNames: "fluid.function", argumentMap: {} }); fluid.defaults("gpii.windows.restartExplorer", { gradeNames: "fluid.function", argumentMap: {} }); fluid.defaults("gpii.windows.killProcessByName", { gradeNames: "fluid.function", argumentMap: { filename: 0 } }); fluid.defaults("gpii.windows.closeProcessByName", { gradeNames: "fluid.function", argumentMap: { filename: 0, options: 1 } });