gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
621 lines (545 loc) • 24.4 kB
JavaScript
/*
* 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
}
});