diff --git a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js index f6cb9ef5f..b388e128f 100644 --- a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js +++ b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js @@ -113,6 +113,10 @@ windows.kernel32 = ffi.Library("kernel32", { // https://msdn.microsoft.com/library/ms683199 "GetModuleHandleW": [ t.HANDLE, ["int"] + ], + // https://msdn.microsoft.com/library/ms683223 + "GetProcessTimes": [ + t.BOOL, [ t.HANDLE, "void*", "void*", "void*", "void*" ] ] }); @@ -198,6 +202,10 @@ windows.user32 = ffi.Library("user32", { // https://msdn.microsoft.com/library/ms632682 "DestroyWindow": [ t.BOOL, [ t.HANDLE ] + ], + // https://msdn.microsoft.com/library/ms633499 + "FindWindowW": [ + gpii.windows.types.HANDLE, ["char*", "char*"] ] }); @@ -231,6 +239,44 @@ gpii.windows.shcore = new ffi.Library("Shcore", { // See GPII-3099. gpii.windows.shcore.SetProcessDpiAwareness(1); + +// https://msdn.microsoft.com/library/ms724284 +windows.FILETIME = new Struct([ + [t.DWORD, "dwLowDateTime"], + [t.DWORD, "dwHighDateTime"] +]); + +// https://msdn.microsoft.com/library/aa373677 +windows.RM_UNIQUE_PROCESS = new Struct([ + [t.DWORD, "dwProcessId"], + [windows.FILETIME, "ProcessStartTime"] +]); +windows.RM_UNIQUE_PROCESSES = arrayType(windows.RM_UNIQUE_PROCESS); + +// Restart Manager API: https://msdn.microsoft.com/library/aa373524 +windows.restartManager = ffi.Library("rstrtmgr", { + // https://msdn.microsoft.com/library/aa373668 + "RmStartSession": [ + t.DWORD, [ t.LPDWORD, t.DWORD, "char*" ] + ], + // https://msdn.microsoft.com/library/aa373663 + "RmRegisterResources": [ + t.DWORD, [ t.DWORD, t.UINT, "char*", t.UINT, windows.RM_UNIQUE_PROCESSES, t.UINT, "void*" ] + ], + // https://msdn.microsoft.com/library/aa373665 + "RmRestart": [ + t.DWORD, [ t.DWORD, t.DWORD, "void*" ] + ], + // https://msdn.microsoft.com/library/aa373667 + "RmShutdown": [ + t.DWORD, [ t.DWORD, t.ULONG, "void*" ] + ], + // https://msdn.microsoft.com/library/aa373659 + "RmEndSession": [ + t.DWORD, [ t.DWORD ] + ] +}); + /** * Gets a function pointer for an EnumWindowsProc callback for EnumWindows. * @@ -268,6 +314,8 @@ windows.API_constants = { // https://msdn.microsoft.com/en-us/library/windows/desktop/ms684880%28v=vs.85%29.aspx PROCESS_TERMINATE: 0x0001, PROCESS_QUERY_LIMITED_INFORMATION: 0x1000, + PROCESS_QUERY_INFORMATION: 0x0400, + PROCESS_VM_READ: 0x0010, // https://msdn.microsoft.com/library/ms685981 SC_MANAGER_CONNECT: 0x0001, @@ -924,6 +972,23 @@ windows.getWindowProcessId = function (hwnd) { return ptr.deref(); }; +/** + * Get the process ID that owns the given window. + * + * @param hwnd {Number} The window handle. + * @return {Number} The process ID. + */ +windows.getWindowProcess = function (hwnd) { + var pid = null; + if (hwnd) { + var buf = ref.alloc(windows.types.DWORD); + windows.user32.GetWindowThreadProcessId(hwnd, buf); + pid = buf.deref(); + } + + return pid; +}; + /** * Waits for a condition, by polling a given function. A promise is returned, resolving when the condition is met or * rejecting upon timeout. If the condition is already met, then the returned promise will be resolved. @@ -993,4 +1058,35 @@ windows.waitForCondition = function (func, options) { return promise; }; +/** + * Returns an Error containing the arguments. + * + * @param message {String} The message. + * @param returnCode {String|Number} [optional] The return code of the function causing the error. + * @param errorCode {String|Number} [optional] The last win32 error (from GetLastError), if already known. + * @return {Error} The error. + */ +windows.win32error = function (message, returnCode, errorCode) { + var err = new Error(windows.win32errorText(message, returnCode, errorCode)); + err.returnCode = returnCode; + err.errorCode = errorCode; + err.isError = true; + return err; +}; + +/** + * Creates an error message for a win32 error. + * + * @param message {String} The message. + * @param returnCode {String|Number} [optional] The return code of the function causing the error. + * @param errorCode {String|Number} [optional] The last win32 error (from GetLastError), if already known. + * @return {Error} The error message. + */ +windows.win32errorText = function (message, returnCode, errorCode) { + var text = "win32 error: " + message; + text += (returnCode === undefined) ? "" : (" return:" + returnCode); + text += " win32:" + (errorCode || windows.kernel32.GetLastError()); + return text; +}; + exports.windows = windows; diff --git a/gpii/node_modules/processHandling/processHandling.js b/gpii/node_modules/processHandling/processHandling.js index 8e518e796..26e995f1f 100644 --- a/gpii/node_modules/processHandling/processHandling.js +++ b/gpii/node_modules/processHandling/processHandling.js @@ -24,94 +24,30 @@ var gpii = fluid.registerNamespace("gpii"); var windows = fluid.registerNamespace("gpii.windows"); require("../WindowsUtilities/WindowsUtilities.js"); +require("../processReporter/processReporter.js"); +require("../registryResolver"); 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} The filename of the application. For example, the Windows on + * @param filename {String} 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 pids = gpii.windows.findProcessByName(filename, true); - if (pids) { - for (var n = 0, len = pids.length; n < len; n++) { - var hProcess = windows.kernel32.OpenProcess(c.PROCESS_TERMINATE, 0, pids[n]); - if (hProcess !== ref.NULL) { - windows.kernel32.TerminateProcess(hProcess, 9); - windows.kernel32.CloseHandle(hProcess); - } - } - } -}; + var procs = processesBridge.findProcessesByCommand(filename); -/** - * Finds a running process with the given name. - * - * The CreateToolhelp32Snapshot Windows API call captures the running processes, and the Process32First/Next - * functions are used to enumerate them. - * - * @param filename The exe file name to search for, null to match any. - * @param all {boolean?} Set to true to return an array containing all matching processes. - * @param fullInfo {boolean?} Set to true to return the pid, ppid, and exe name of matching processes. - * @returns {?number|number[]} The Process ID of the matching processes, otherwise null. - */ -gpii.windows.findProcessByName = function (filename, all, fullInfo) { - - // Get a snapshot of the processes. - var hSnapShot = windows.kernel32.CreateToolhelp32Snapshot(windows.API_constants.TH32CS_SNAPPROCESS, null); - if (hSnapShot === windows.API_constants.INVALID_HANDLE_VALUE) { - console.error("CreateToolhelp32Snapshot failed. Win32 error: " + windows.GetLastError()); - return null; - } - - var matches = []; - var filenameLower = filename && filename.toLowerCase(); - - try { - // Create the structure for the return parameter of Process32First/Next. - var pEntry = new windows.PROCESSENTRY32(); - pEntry.dwSize = windows.PROCESSENTRY32.size; - - // Enumerate the processes. - var hRes = windows.kernel32.Process32First(hSnapShot, pEntry.ref()); - while (hRes) { - var buf = new Buffer(pEntry.szExeFile); - var processName = ref.readCString(buf, 0); - - if (!filename || processName.toLowerCase() === filenameLower) { - var proc; - if (fullInfo) { - proc = { - pid: pEntry.th32ProcessID, - ppid: pEntry.th32ParentProcessID, - exeFile: processName - }; - } else { - proc = pEntry.th32ProcessID; - } - if (all) { - // Add it to the array of matches. - matches.push(proc); - } else { - // Only want the first one - return it. - return proc; - } - } - - hRes = windows.kernel32.Process32Next(hSnapShot, pEntry.ref()); - } - } finally { - // Make sure the snapshot is closed. - if (hSnapShot) { - windows.kernel32.CloseHandle(hSnapShot); + for (var n = 0, len = procs.length; n < len; n++) { + var hProcess = windows.kernel32.OpenProcess(c.PROCESS_TERMINATE, 0, procs[n].pid); + if (hProcess !== ref.NULL) { + windows.kernel32.TerminateProcess(hProcess, 9); + windows.kernel32.CloseHandle(hProcess); } } - - return all ? matches : null; }; /** @@ -123,7 +59,7 @@ gpii.windows.findProcessByName = function (filename, all, fullInfo) { gpii.windows.isProcessRunning = function (proc) { var togo = false; if (isNaN(proc)) { - togo = gpii.windows.findProcessByName(proc) !== null; + togo = gpii.processReporter.find(proc); } else { try { process.kill(proc, 0); @@ -135,7 +71,6 @@ gpii.windows.isProcessRunning = function (proc) { 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. @@ -234,7 +169,9 @@ gpii.windows.closeProcessByName = function (filename, options) { options = fluid.extend(defaultOptions, options); - var pids = gpii.windows.findProcessByName(filename, true); + var pids = fluid.transform(processesBridge.findProcessesByCommand(filename), function (proc) { + return proc.pid; + }); if (!pids) { // Process is not running. return fluid.toPromise(true); @@ -296,60 +233,6 @@ gpii.windows.closeProcessByName = function (filename, options) { return promiseTogo; }; -/** - * Gets the path to the executable of a running process. - * - * Provide the processHandle if it's available when calling, otherwise provide the pid. - * - * @param pid {Number} [Optional] The process ID. - * @param processHandle [Optional] The process handle. - * @return {String} The path to the executable. - */ -gpii.windows.getProcessPath = function (pid, processHandle) { - var hProcess; - var togo = null; - - if (processHandle) { - hProcess = processHandle; - } else if (pid) { - hProcess = - gpii.windows.kernel32.OpenProcess(gpii.windows.API_constants.PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); - } else { - fluid.fail("Either pid or processHandle needs to be given."); - } - - if (hProcess) { - try { - var size = ref.alloc(gpii.windows.types.DWORD); - size.writeUInt32LE(gpii.windows.API_constants.MAX_PATH, 0); - var path = new Buffer(gpii.windows.API_constants.MAX_PATH); - - var success = gpii.windows.kernel32.QueryFullProcessImageNameW(hProcess, 0, path, size); - if (success) { - togo = gpii.windows.fromWideChar(path); - } - } finally { - if (hProcess !== processHandle) { - gpii.windows.kernel32.CloseHandle(hProcess); - } - } - } - - if (!togo) { - // OpenProcess or QueryFullProcessImageName failed (usually due to permissions). Get the file name from the - // process list instead. - var all = gpii.windows.findProcessByName(null, true, true); - var process = all.find(function (p) { - return p.pid === pid; - }); - if (process) { - togo = process.exeFile; - } - } - return togo; - -}; - /** * Determines the current state of a service, which can be one of paused, running, or stopped: * @@ -404,6 +287,175 @@ gpii.windows.getServiceState = function (serviceName) { return state; }; +/** + * Gets the creation time of a process. + * @param pid {number} The process ID. + * @return {FILETIME} The creation time. + */ +gpii.windows.getProcessCreationTime = function (pid) { + var togo = null; + var hProcess = gpii.windows.kernel32.OpenProcess( + windows.API_constants.PROCESS_VM_READ | windows.API_constants.PROCESS_QUERY_INFORMATION, 0, pid); + + if (hProcess) { + var chSize = ref.alloc(windows.types.DWORD); + chSize.fill(0); + + var creation = new gpii.windows.FILETIME(), + exit = new gpii.windows.FILETIME(), + kernel = new gpii.windows.FILETIME(), + user = new gpii.windows.FILETIME(); + + var success = windows.kernel32.GetProcessTimes( + hProcess, creation.ref(), exit.ref(), kernel.ref(), user.ref()); + + if (success) { + togo = creation; + } + } + + return togo; +}; + +/** + * Gets the PID of the explorer process that owns the tasktray. + */ +gpii.windows.getExplorerProcess = function () { + var classname = windows.stringToWideChar("Shell_TrayWnd"); + var trayWindow = windows.user32.FindWindowW(classname, ref.NULL); + return windows.getWindowProcessId(trayWindow); +}; + +gpii.windows.currentRestartSession = null; + +/** + * Stops the Windows Explorer shell process, in a way that it can be restarted again so the folder windows are restored. + * + * Use restartExplorer() to restart it. + * + * @return {Promise} Resolves when explorer has stopped. + */ +gpii.windows.stopExplorer = function () { + var promise = fluid.promise(); + var session = null; + var rm = windows.restartManager; + + // RestartManager.h: + var CCH_RM_SESSION_KEY = 16 * 2 + 1; // sizeof(GUID) * 2 + 1 + var RmShutdownOnlyRegistered = 10; + + // Get the explorer process that owns the task tray. + var explorerPid = windows.getExplorerProcess(); + if (explorerPid) { + var creationTime = gpii.windows.getProcessCreationTime(explorerPid); + var processArray = new windows.RM_UNIQUE_PROCESSES(1); + processArray[0].dwProcessId = explorerPid; + processArray[0].ProcessStartTime = creationTime; + + // Create the "restart session". + var sessionBuf = ref.alloc(gpii.windows.types.DWORD); + var sessionKey = new Buffer(CCH_RM_SESSION_KEY * 2); + sessionKey.fill(0); + var result = rm.RmStartSession(sessionBuf, 0, sessionKey); + + if (result) { + promise.reject(windows.win32error("RmStartSession", result)); + } else { + session = sessionBuf.deref(); + + // Close the session upon failure. + promise.then(null, function () { + rm.RmEndSession(session); + }); + + // Add the explorer process to the restart list. + result = rm.RmRegisterResources(session, 0, ref.NULL, processArray.length, processArray, 0, ref.NULL); + if (result) { + promise.reject(windows.win32error("RmRegisterResources", result)); + } else { + // Close the process. This waits for it to shutdown nicely, so it takes a few seconds. + rm.RmShutdown.async(session, RmShutdownOnlyRegistered, ref.NULL, function (err, ret) { + if (err) { + promise.reject({ + isError: true, + error: err + }); + } else if (ret) { + promise.reject(windows.win32error("RmShutdown", ret)); + } else { + gpii.windows.currentRestartSession = session; + promise.resolve(session); + } + }); + } + } + } else { + promise.reject({ + isError: true, + error: "Unable to find the Explorer process" + }); + } + + return promise; +}; + +/** + * Restarts the Windows Explorer shell process. + * + * stopExplorer will be called if it hasn't been called already. + */ +gpii.windows.restartExplorer = function () { + var promiseTogo = fluid.promise(); + var rm = windows.restartManager; + + // Call stopExplorer if there's no current restart session. + var shutdownPromise = (gpii.windows.currentRestartSession === null) + ? gpii.windows.stopExplorer() + : fluid.toPromise(gpii.windows.currentRestartSession); + + shutdownPromise.then(function (session) { + try { + // Re-start the explorer process. + var result = rm.RmRestart(session, 0, ref.NULL); + if (result) { + throw windows.win32error("RmRestart", result); + } + + } finally { + rm.RmEndSession(session); + gpii.windows.currentRestartSession = null; + } + promiseTogo.resolve(); + }); + + return promiseTogo; +}; + +/** + * Updates the Windows display language, by restarting explorer if the language has changed since the last time + * this was called. + * + * @param {String} currentLanguage - The current (new) language. + */ +gpii.windows.updateLanguage = function (currentLanguage) { + if (gpii.windows.updateLanguage.lastLanguage !== currentLanguage) { + gpii.windows.updateLanguage.lastLanguage = currentLanguage; + return gpii.windows.restartExplorer(); + } +}; + +gpii.windows.updateLanguage.lastLanguage = + gpii.windows.readRegistryKey("HKEY_CURRENT_USER", "Control Panel\\Desktop", "PreferredUILanguages", "REG_SZ").value; + +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", @@ -419,3 +471,10 @@ fluid.defaults("gpii.windows.closeProcessByName", { options: 1 } }); + +fluid.defaults("gpii.windows.updateLanguage", { + gradeNames: "fluid.function", + argumentMap: { + currentLanguage: 0 + } +}); diff --git a/gpii/node_modules/processHandling/test/testProcessHandling.js b/gpii/node_modules/processHandling/test/testProcessHandling.js index 561837977..622063ccb 100644 --- a/gpii/node_modules/processHandling/test/testProcessHandling.js +++ b/gpii/node_modules/processHandling/test/testProcessHandling.js @@ -23,14 +23,19 @@ var gpii = fluid.registerNamespace("gpii"); var shelljs = require("shelljs"); var path = require("path"); var child_process = require("child_process"); +var ffi = require("ffi"); +var arrayType = require("ref-array"); +var Struct = require("ref-struct"); require("../processHandling.js"); fluid.registerNamespace("gpii.tests.windows.processHandling"); + var waitExe = "gpii-process-handling-test.exe"; var waitExePath = null; +var teardowns = []; jqUnit.module("gpii.tests.windows.processHandling", { setup: function () { // Take a copy of the built-in "waitfor" command, to ensure a unique process name. @@ -39,6 +44,10 @@ jqUnit.module("gpii.tests.windows.processHandling", { shelljs.cp(path.join(process.env.SystemRoot, "/System32/waitfor.exe"), waitExePath); }, teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + if (waitExePath !== null) { gpii.windows.killProcessByName(waitExe); shelljs.rm(waitExePath); @@ -46,27 +55,6 @@ jqUnit.module("gpii.tests.windows.processHandling", { } }); -jqUnit.test("Testing findProcessByName", function () { - // Find a process that isn't running. - var pid = gpii.windows.findProcessByName("a non-matching process.exe"); - jqUnit.assertEquals("Process should not have been found running", null, pid); - - // Find a process that is running. - pid = gpii.windows.findProcessByName("node.exe"); - jqUnit.assertNotEquals("Process should have been found running", null, pid); - - // Find a process that is running, full details - var details = gpii.windows.findProcessByName("node.exe", false, true); - jqUnit.assertNotEquals("Process should have been found running (fullInfo)", null, details); - jqUnit.assertTrue("pid should be a number", typeof(details.pid) === "number"); - jqUnit.assertTrue("ppid should be a number", typeof(details.ppid) === "number"); - jqUnit.assertEquals("exeFile should be node.exe", "node.exe", details.exeFile.toLowerCase()); - - // Find multiple processes. There's always more than one svchost.exe running on Windows. - var pids = gpii.windows.findProcessByName("svchost.exe", true); - jqUnit.assertTrue("Should have found several matching process.", pids.length > 1); -}); - jqUnit.test("Testing isProcessRunning", function () { // Check a PID that isn't running. var running = gpii.windows.isProcessRunning(-1); @@ -118,8 +106,8 @@ jqUnit.asyncTest("Testing timeout waiting for processes termination", function ( jqUnit.asyncTest("Testing waiting for processes start and end", function () { jqUnit.expect(3); - var pid = gpii.windows.findProcessByName(waitExe); - jqUnit.assertEquals("The process should not already be running.", null, pid); + var running = gpii.processReporter.find(waitExe); + jqUnit.assertFalse("The process should not already be running.", running); // Timeout waiting for the process start/end after 10 seconds. var options = { timeout: 10000 }; @@ -151,8 +139,8 @@ jqUnit.asyncTest("Testing waiting for processes start and end", function () { jqUnit.asyncTest("Testing Killing Processes", function () { jqUnit.expect(3); - var pid = gpii.windows.findProcessByName(waitExe); - jqUnit.assertEquals("The process should not already be running.", null, pid); + var running = gpii.processReporter.find(waitExe); + jqUnit.assertFalse("The process should not already be running.", running); // On the call below, async is true because if it is false shelljs will // wait around until it is manually killed before continuing with the @@ -240,49 +228,186 @@ jqUnit.asyncTest("Testing closeProcessByName (window-less process)", function () child_process.exec(command); }); -jqUnit.asyncTest("Testing getProcessPath", function () { +jqUnit.test("Testing getServiceState", function () { + // Local Session Manager will always be running. + var state = gpii.windows.getServiceState("LSM"); + jqUnit.assertEquals("LSM service should be running", "running", state); + + // There's a chance it might be running, but who sends faxes anymore? + state = gpii.windows.getServiceState("Fax"); + jqUnit.assertEquals("Fax service should be stopped", "stopped", state); + + state = gpii.windows.getServiceState("gpii-unknown"); + jqUnit.assertEquals("gpii-unknown service should be unknown", "unknown", state); +}); + +// Check if getExplorerProcess returns an explorer.exe pid. +jqUnit.test("Testing getExplorerProcess", function () { + + var explorerPid = gpii.windows.getExplorerProcess(); + + jqUnit.assertFalse("explorer PID must be a number", isNaN(explorerPid)); + + var isRunning = gpii.windows.isProcessRunning(explorerPid); + jqUnit.assertTrue("explorer pid must be a running process", isRunning); + + var processes = gpii.processes.windows.getProcessList(explorerPid); + jqUnit.assertEquals("getProcessList should return 1 process", 1, processes.length); + jqUnit.assertEquals("process name should be explorer.exe", "explorer.exe", processes[0].command.toLowerCase()); +}); + + +var t = gpii.windows.types; +// https://msdn.microsoft.com/library/aa373674 +var CCH_RM_MAX_SVC_NAME = 63; +var CCH_RM_MAX_APP_NAME = 255; +gpii.tests.windows.processHandling.RM_PROCESS_INFO = new Struct([ + [ gpii.windows.RM_UNIQUE_PROCESS, "Process"], + [ arrayType("char", (CCH_RM_MAX_APP_NAME + 1) * 2), "strAppName"], + [ arrayType("char", (CCH_RM_MAX_SVC_NAME + 1) * 2), "strServiceShortName"], + [ "int32", "ApplicationType"], + [ "uint32", "AppStatus"], + [ "uint32", "TSSessionId"], + [ "int32", "bRestartable"] +]); +gpii.tests.windows.processHandling.RM_PROCESS_INFOS = + arrayType(gpii.tests.windows.processHandling.RM_PROCESS_INFO); + +gpii.tests.windows.processHandling.restartManager = ffi.Library("rstrtmgr", { + // https://msdn.microsoft.com/library/aa373661 + "RmGetList": [ + t.DWORD, [ t.DWORD, "uint*", "uint*", gpii.tests.windows.processHandling.RM_PROCESS_INFOS, t.LPDWORD ] + ] +}); + +/** + * Check if the restart manager session has been set up correctly by inspecting the result of RmGetList. + * + * @param stage {string} The stage of the restart process (for logging). + * @param sessionHandle {Number} The session handle. + * @param pids {Number[]} The process ids that are expected to be stopped/restarted. + */ +gpii.tests.windows.processHandling.checkRestartManager = function (stage, sessionHandle, pids) { + jqUnit.expect(3 + pids.length * 3); + + var messagePrefix = "After " + stage + ": "; + + // RM_APP_STATUS: https://msdn.microsoft.com/library/aa373669 + var RmStatusRunning = 1; + + var procCount = pids.length; + var procInfoNeededBuf = (new Buffer(4)).fill(0); + var procInfoCountBuf = new Buffer(4); + procInfoCountBuf.writeUInt32LE(procCount); + + var procInfos = new gpii.tests.windows.processHandling.RM_PROCESS_INFOS(procCount); + + var rebootReasons = new Buffer(4); + + var result = gpii.tests.windows.processHandling.restartManager.RmGetList( + sessionHandle, procInfoNeededBuf, procInfoCountBuf, procInfos, rebootReasons); + + jqUnit.assertEquals(messagePrefix + "RmGetList should return 0 (ERROR_SUCCESS)", 0, result); + + var procInfoCount = procInfoCountBuf.readUInt32LE(); + jqUnit.assertEquals(messagePrefix + "Number of procInfos returned by RmGetList should be the same number of pids", + pids.length, procInfoCount); + + var pidsFound = []; + for (var n = 0; n < procInfoCount; n++) { + var p = procInfos[n]; + pidsFound.push(p.Process.dwProcessId); + + var index = pids.indexOf(p.Process.dwProcessId); + jqUnit.assertTrue(messagePrefix + "RmGetList should only return expected pids", index > -1); + + jqUnit.assertEquals(messagePrefix + "Process should be running (AppStatus = RmStatusRunning)", + RmStatusRunning, p.AppStatus); + jqUnit.assertTrue(messagePrefix + "Process should be restartable", + RmStatusRunning, !!p.bRestartable); + } + + jqUnit.assertDeepEq(messagePrefix + "RmGetList should return all pids", + pids.sort(), pidsFound.sort()); +}; + +/** + * Replaces RmShutdown and RmRestart with an implementation that just checks if they have been called properly. + * + * @param expectedPids {Number[]} The processes that should have been restarted. + * @param restart {boolean} True if a call to RmRestart is expected. + */ +gpii.tests.windows.processHandling.mockRestartManager = function (expectedPids) { + jqUnit.expect(7); + + expectedPids = fluid.copy(expectedPids); + var rmShutdown = gpii.windows.restartManager.RmShutdown.async; + var rmRestart = gpii.windows.restartManager.RmRestart; + teardowns.push(function () { + gpii.windows.restartManager.RmShutdown = rmShutdown; + gpii.windows.restartManager.RmRestart = rmRestart; + }); + + gpii.windows.restartManager.RmShutdown.async = function (dwSessionHandle, lActionFlags, fnStatus, callback) { + var RmShutdownOnlyRegistered = 10; + jqUnit.assertFalse("RmShutdown dwSessionHandle must be a number", isNaN(dwSessionHandle)); + jqUnit.assertEquals( + "RmShutdown lActionFlags must be RmShutdownOnlyRegistered", RmShutdownOnlyRegistered, lActionFlags); + // For some reason, ref doesn't like being required from this script. + jqUnit.assertEquals("RmShutdown fnStatus must be ref.NULL", 0, fnStatus && fnStatus.length); + jqUnit.assertEquals("RmShutdown callback must be a function", "function", typeof(callback)); + + gpii.tests.windows.processHandling.checkRestartManager("RmShutdown", dwSessionHandle, expectedPids); + + // Invoke the callback with a successful return. + callback(gpii.windows.API_constants.returnCodes.ERROR_SUCCESS); + }; + gpii.windows.restartManager.RmRestart = function (dwSessionHandle, dwRestartFlags, fnStatus) { + jqUnit.assertFalse("RmShutdown dwSessionHandle must be a number", isNaN(dwSessionHandle)); + // "Reserved. This parameter should be 0." + jqUnit.assertEquals("RmShutdown dwRestartFlags must be 0", 0, dwRestartFlags); + jqUnit.assertEquals("RmShutdown fnStatus must be ref.NULL", 0, fnStatus && fnStatus.length); + }; +}; + +jqUnit.asyncTest("Testing stopExplorer + restartExplorer", function () { + + // Restarting explorer for real isn't a good idea, because it could fail - if it's guaranteed to succeed, then the + // test is not required. + // So, the "dangerous" functions (RmShutdown and RmRestart) are mocked to check they're being called correctly. + jqUnit.expect(3); + var explorerPid = gpii.windows.getExplorerProcess(); - var exeName = waitExe; - var exePath = waitExePath; - var args = [ "getProcessPathTest", "/T", "30"]; + gpii.tests.windows.processHandling.mockRestartManager([ explorerPid ], true); - fluid.log("Executing " + exePath); - var child = child_process.spawn(exePath, args); + var stopPromise = gpii.windows.stopExplorer(); - gpii.windows.waitForProcessStart(exeName) - .then(function () { - jqUnit.assert("Process started"); + jqUnit.assertTrue("stopExplorer must return a promise", fluid.isPromise(stopPromise)); - // Test with the PID. - var result1 = gpii.windows.getProcessPath(child.pid); - jqUnit.assertEquals("The exe path should be correct (pid)", exePath, result1); + stopPromise.then(function () { + jqUnit.assert("restartProcesses promise resolved"); - // Test with the process handle. - var hProcess = gpii.windows.kernel32.OpenProcess( - gpii.windows.API_constants.PROCESS_QUERY_LIMITED_INFORMATION, 0, child.pid); + var restartPromise = gpii.windows.restartExplorer(); - try { - var result2 = gpii.windows.getProcessPath(null, hProcess); - jqUnit.assertEquals("The exe path should be correct (processHandle)", exePath, result2); - } finally { - gpii.windows.kernel32.CloseHandle(hProcess); - } + jqUnit.assertTrue("restartExplorer must return a promise", fluid.isPromise(restartPromise)); + + restartPromise.then(jqUnit.start, fluid.fail); + + }, jqUnit.fail); - child.kill(); - }); - child.on("close", jqUnit.start); }); -jqUnit.test("Testing getServiceState", function () { - // Local Session Manager will always be running. - var state = gpii.windows.getServiceState("LSM"); - jqUnit.assertEquals("LSM service should be running", "running", state); +jqUnit.asyncTest("Testing restartExplorer", function () { + jqUnit.expect(1); + var explorerPid = gpii.windows.getExplorerProcess(); - // There's a chance it might be running, but who sends faxes anymore? - state = gpii.windows.getServiceState("Fax"); - jqUnit.assertEquals("Fax service should be stopped", "stopped", state); + gpii.tests.windows.processHandling.mockRestartManager([ explorerPid ], true); + + var restartPromise = gpii.windows.restartExplorer(); + + jqUnit.assertTrue("restartExplorer must return a promise", fluid.isPromise(restartPromise)); + + restartPromise.then(jqUnit.start, fluid.fail); - state = gpii.windows.getServiceState("gpii-unknown"); - jqUnit.assertEquals("gpii-unknown service should be unknown", "unknown", state); }); diff --git a/gpii/node_modules/processReporter/dotNetProcesses.csx b/gpii/node_modules/processReporter/dotNetProcesses.csx index 6eeec6232..70c93be17 100644 --- a/gpii/node_modules/processReporter/dotNetProcesses.csx +++ b/gpii/node_modules/processReporter/dotNetProcesses.csx @@ -51,7 +51,8 @@ public class Startup } } else { ManagementObject[] someProcesses = Array.FindAll( - processes, p => p.GetPropertyValue("Name").ToString() == input + processes, + p => string.Equals(p.GetPropertyValue("Name").ToString(), input, StringComparison.OrdinalIgnoreCase) ); foreach (ManagementObject p in someProcesses) { makeAndAppendProcInfo(p, result); @@ -119,5 +120,6 @@ public class Startup } // Process no longer running -- nothing to add. catch (ArgumentException e) { } + catch (InvalidOperationException e) { } } } diff --git a/gpii/node_modules/processReporter/processesBridge.js b/gpii/node_modules/processReporter/processesBridge.js index e461d5377..30d2db198 100644 --- a/gpii/node_modules/processReporter/processesBridge.js +++ b/gpii/node_modules/processReporter/processesBridge.js @@ -36,7 +36,8 @@ fluid.defaults("gpii.processes.windows", { args: ["{arguments}.0"] // process name or id, optional. } - } + }, + ignoreCase: true }); /** diff --git a/gpii/node_modules/processReporter/test/processesBridge_tests.js b/gpii/node_modules/processReporter/test/processesBridge_tests.js index ef4f607b1..6813b86cc 100644 --- a/gpii/node_modules/processReporter/test/processesBridge_tests.js +++ b/gpii/node_modules/processReporter/test/processesBridge_tests.js @@ -133,6 +133,25 @@ jqUnit.test( } ); +jqUnit.test( + "Test findProcessesByCmd()/findFirstProcessByCmd() with nodejs itself - ignoring case", + function () { + var nodeProcInfos = processesBridge.findProcessesByCommand("NoDE.eXe"); + jqUnit.assertNotEquals( + "Getting all 'NoDE.eXe' processes", 0, nodeProcInfos.length + ); + nodeProcInfos.forEach(function (aProcInfo) { + jqUnit.assertEquals( + "Node commmand name", "node.exe", aProcInfo.command + ); + }); + var procInfo = processesBridge.findFirstProcessByCommand("NoDE.eXe"); + jqUnit.assertNotNull( + "Looking for first 'NoDE.eXe' processes", procInfo); + jqUnit.assertEquals("Node commmand name", "node.exe", procInfo.command); + } +); + jqUnit.test( "Test initProcInfoNotRunning()", function () { diff --git a/gpii/node_modules/registeredAT/test/testRegisteredAT.js b/gpii/node_modules/registeredAT/test/testRegisteredAT.js index b5d323608..49a80a58a 100644 --- a/gpii/node_modules/registeredAT/test/testRegisteredAT.js +++ b/gpii/node_modules/registeredAT/test/testRegisteredAT.js @@ -140,8 +140,8 @@ jqUnit.asyncTest("Testing AT start and stop", function () { }); }); - var pid = gpii.windows.findProcessByName(testData.ATExe); - jqUnit.assertEquals("The process should not already be running.", null, pid); + var running = gpii.processReporter.find(testData.ATExe); + jqUnit.assertFalse("The process should not already be running.", running); gpii.windows.startRegisteredAT(testData.appName, {atInfo: testData}); @@ -185,8 +185,8 @@ jqUnit.asyncTest("Testing enableRegisteredAT.set/get functions", function () { }); }); - var pid = gpii.windows.findProcessByName(testData.ATExe); - jqUnit.assertEquals("The process should not already be running.", null, pid); + var isRunning = gpii.windows.isProcessRunning(testData.ATExe); + jqUnit.assertFalse("The process should not already be running.", isRunning); gpii.windows.startRegisteredAT(testData.appName, {atInfo: testData}); diff --git a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js index 1a7aca1b2..e37be3499 100644 --- a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js +++ b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js @@ -22,7 +22,10 @@ var ffi = require("ffi"); var fluid = require("gpii-universal"); -var windows = fluid.registerNamespace("gpii.windows"); +var gpii = fluid.registerNamespace("gpii"), + windows = fluid.registerNamespace("gpii.windows"); + +var processesBridge = gpii.processes.windows(); fluid.require("%gpii-windows/gpii/node_modules/WindowsUtilities/WindowsUtilities.js"); @@ -253,7 +256,7 @@ windows.checkActiveWindow = function (that) { // Log how long the last window was active. windows.logAppActivate(that); - var exePath = windows.getProcessPath(activePid); + var exePath = processesBridge.getProcessPath(activePid); that.state.application.currentProcess.pid = activePid; that.state.application.currentProcess.exe = exePath; diff --git a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js index 03b3218d4..ba2cccd56 100644 --- a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js +++ b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js @@ -692,13 +692,16 @@ jqUnit.asyncTest("Testing application metrics", function () { windowsMetrics.config.application.precision = 1; + var processesBridge = gpii.processes.windows(); + // Pick three windows (owned by different processes) that already exist, and pretend they've became active by // mocking GetForegroundWindow. var windows = []; var exes = {}; gpii.windows.enumerateWindows(function (hwnd) { if (windows.length < 3) { - var exe = gpii.windows.getProcessPath(gpii.windows.getWindowProcessId(hwnd)); + + var exe = processesBridge.getProcessPath(gpii.windows.getWindowProcessId(hwnd)); if (!exes[exe]) { exes[exe] = true; windows.push({ diff --git a/package.json b/package.json index 1f17e0a81..9978975b4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "edge-js": "8.8.1", "ffi": "2.0.0", - "gpii-universal": "0.3.0-dev.20180711T150609Z.2bb2ecf", + "gpii-universal": "stegru/universal#GPII-2212", "@pokusew/pcsclite": "0.4.18", "ref": "1.3.4", "ref-struct": "1.1.0",