diff --git a/Gruntfile.js b/Gruntfile.js index b0a01b1a0..daf93c958 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,6 +13,25 @@ https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt "use strict"; module.exports = function (grunt) { + grunt.loadNpmTasks("grunt-jsonlint"); + grunt.loadNpmTasks("grunt-shell"); + grunt.loadNpmTasks("fluid-grunt-eslint"); + + var gypCompileCmd = "node-gyp configure build"; + var gypCleanCmd = "node-gyp clean"; + + function nodeGypShell(cmd, cwd) { + return { + options: { + execOptions: { + cwd: cwd + } + }, + command: cmd + }; + } + + grunt.registerTask("lint", "Apply jshint and jsonlint", ["eslint", "jsonlint"]); grunt.initConfig({ eslint: { @@ -20,11 +39,37 @@ module.exports = function (grunt) { }, jsonlint: { src: ["gpii/**/*.json", "tests/**/*.json", "examples/**/*.json", "./*.json"] + }, + shell: { + options: { + stdout: true, + stderr: true, + failOnError: true, + // A large maxBuffer value is required for the 'runAcceptanceTests' task otherwise + // a 'stdout maxBuffer exceeded' warning is generated. + execOptions: { + maxBuffer: 1000 * 1024 + } + }, + compileTabletMode: nodeGypShell(gypCompileCmd, "gpii/node_modules/tabletMode/src"), + cleanTabletMode: nodeGypShell(gypCleanCmd, "gpii/node_modules/tabletMode/src") } }); - grunt.loadNpmTasks("grunt-jsonlint"); - grunt.loadNpmTasks("fluid-grunt-eslint"); + grunt.registerTask("build-addons", "Build the native addons", function () { + grunt.task.run("shell:compileTabletMode"); + }); + + grunt.registerTask("build", "Build the entire GPII", function () { + grunt.task.run("build-addons"); + }); + + grunt.registerTask("clean-addons", "Clean the native addons", function () { + grunt.task.run("shell:cleanTabletMode"); + }); + + grunt.registerTask("clean", "Clean the GPII binaries and uninstall", function () { + grunt.task.run("clean-addons"); + }); - grunt.registerTask("lint", "Apply jshint and jsonlint", ["eslint", "jsonlint"]); }; diff --git a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js index 1c078e93a..1f1d560db 100644 --- a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js +++ b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js @@ -25,6 +25,7 @@ var gpii = fluid.registerNamespace("gpii"); var windows = fluid.registerNamespace("gpii.windows"); var ref = require("ref"); +var refWChar = require("ref-wchar"); var Struct = require("ref-struct"); var arrayType = require("ref-array"); var NULL = ref.NULL; @@ -113,6 +114,15 @@ 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*" ] + ], + // https://msdn.microsoft.com/library/aa373344 + "GetApplicationRestartSettings": [ + //t.HANDLE, [ t.HANDLE, "char*", t.LPDWORD, t.LPDWORD ] + t.HANDLE, [ t.HANDLE, t.HANDLE, t.LPDWORD, t.HANDLE] ] }); @@ -158,6 +168,10 @@ windows.user32 = ffi.Library("user32", { // HMONITOR, LPMONITORINFO t.BOOL, [t.HANDLE, "pointer"] ], + // https://msdn.microsoft.com/library/ms633499 + "FindWindowW": [ + t.HANDLE, ["char*", "char*" ] + ], // https://msdn.microsoft.com/library/ms632680 "CreateWindowExW": [ t.HANDLE, [ @@ -201,6 +215,44 @@ windows.user32 = ffi.Library("user32", { ] }); + +// 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 ] + ] +}); + gpii.windows.advapi32 = new ffi.Library("advapi32", { // https://msdn.microsoft.com/library/ms684323 "OpenSCManagerW": [ @@ -257,6 +309,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, @@ -291,6 +345,11 @@ windows.API_constants = { // https://msdn.microsoft.com/library/aa363480 WM_DEVICECHANGE: 0x219, + // https://msdn.microsoft.com/library/aa376890 + WM_QUERYENDSESSION: 0x11, + ENDSESSION_CLOSEAPP: 1, + // https://msdn.microsoft.com/library/aa376889 + WM_ENDSESSION: 0x16, // https://msdn.microsoft.com/library/aa363205 DBT_DEVNODES_CHANGED: 0x7, DBT_DEVICEARRIVAL: 0x8000, @@ -324,7 +383,13 @@ windows.API_constants = { // https://msdn.microsoft.com/en-us/library/windows/hardware/ff554003.aspx DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED: 11, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED: 13, - DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL: 0x80000000 + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL: 0x80000000, + + // https://msdn.microsoft.com/library/ms724947 + SpiFlags: { + SPIF_UPDATEINIFILE: 0x1, + SPIF_SENDCHANGE: 0x2 + } }; fluid.each(windows.API_constants.returnCodesLookup, function (value, key) { @@ -532,10 +597,24 @@ windows.actionConstants = { "SPI_SETMOUSETRAILS": 0x005D, "SPI_GETHIGHDPI": 0x00A5, // not defined in the SDK - "SPI_SETHIGHDPI": 0x00A6 + "SPI_SETHIGHDPI": 0x00A6, + "SPI_SETDESKWALLPAPER": 0x0014, + "SPI_GETDESKWALLPAPER": 0x0073 + // TODO Define additional actions used in calls to SystemParametersInfo here. }; + +/** + * Contains flags used in the "fWinIni" paramater of the function SystemParametersInfo. + * https://msdn.microsoft.com/en-us/library/windows/desktop/ms724947(v=vs.85).aspx + */ +windows.fWinIniFlags = { + "SPIF_UPDATEINIFILE": 0x0001, + "SPIF_SENDWININICHANGE": 0x0002, + "SPIF_SENDCHANGE": 0x0002 +}; + /** * Contains flags used in the "dwFlags" field of various structures * that are used in calls to the SystemParametersInfo function. @@ -752,25 +831,70 @@ windows.createEmptyStructure = function (structName) { return struct; }; +windows.createStructArray = function (itemType, itemCount) { + return new (arrayType(itemType))(itemCount); +}; + /** * Takes an array of flag names, applies binary OR among them and returns the result. * Used to supply the "dwFlags" argument of some structures. * - * @param {Array} flagNamesArray An array of flag names. - * These should be predefined in windows.flagConstants. + * @param {Array} flagNamesArray An array of flag names, which must be in allFlags. + * @param {Array} allFlags [optional] An array of all possible flags and their value. Defaults to windows.flagConstants. + * @param {String} op [optional] The operation, either "or" (default) or "and". */ -windows.combineFlags = function (flagNamesArray) { +windows.combineFlags = function (flagNamesArray, allFlags, op) { + allFlags = allFlags || windows.flagConstants; + var or = op !== "and" && op !== "&"; + + var combine = or + ? function (left, right) { return left | right; } + : function (left, right) { return left & right; }; + var combinedFlags = 0; if (!fluid.isArrayable(flagNamesArray)) { - console.log("GPII Windows SpiSettingsHandler combineFlags: array expected!"); + fluid.log("GPII Windows SpiSettingsHandler combineFlags: array expected!"); return 0; } for (var index in flagNamesArray) { - combinedFlags = combinedFlags | windows.flagConstants[flagNamesArray[index]]; + combinedFlags = combine(combinedFlags, allFlags[flagNamesArray[index]]); } return combinedFlags; }; +/** + * Resolves a list of flags into the numeric value. Flags can either be a string, with the delimiter of either + * "|" or "&" specifying the operation, or an array of flag strings (in which case the operation is binary OR). + * + * The value of each possible flag is specified in allFlags. + * + * @param {String|Array} flags The list of flags to resolve. + * @param {Array} allFlags An associative array of every flag. + * @return {number} The numeric value of flags. + */ +windows.resolveFlags = function (flags, allFlags) { + var togo = 0; + if (!isNaN(Number(flags))) { + // Use the hard-coded number as-is + togo = Number(flags); + } else if (flags) { + var op = "or"; + // Split a string into an array. + if (typeof(flags) === "string") { + var or = flags.indexOf("&") < 0; + op = or ? "or" : "and"; + flags = flags.split(/ *[&|]+ */); + } + if (fluid.isArrayable(flags)) { + togo = gpii.windows.combineFlags(flags, allFlags, op); + } else { + fluid.fail("windows.resolveFlags was passed an unknown type of flags"); + } + } + + return togo; +}; + /** * Gets the value of a given flag name. * @@ -847,9 +971,16 @@ windows.accessFlag = function (object, flagName, setValue) { windows.arrayToBuffer = function (array, type) { var size = ref.coerceType(windows.types[type]).size; var buf = new Buffer(array.length * size); + var i = 0; - for (var i = 0; i < array.length; ++i) { - ref.set(buf, i * size, array[i], windows.types[type]); + if (type === "TCHAR") { + for (i = 0; i < array.length; ++i) { + refWChar.set(buf, i * size, array[i]); + } + } else { + for (i = 0; i < array.length; ++i) { + ref.set(buf, i * size, array[i], windows.types[type]); + } } return buf; @@ -865,9 +996,16 @@ windows.arrayToBuffer = function (array, type) { windows.bufferToArray = function (buffer, type) { var array = []; var size = ref.coerceType(windows.types[type]).size; + var i = 0; - for (var i = 0; i < buffer.length / size; ++i) { - array[i] = ref.get(buffer, i * size, windows.types[type]); + if (type === "TCHAR") { + for (i = 0; i < buffer.length / size; ++i) { + array[i] = refWChar.get(buffer, i * size); + } + } else { + for (i = 0; i < buffer.length / size; ++i) { + array[i] = ref.get(buffer, i * size, windows.types[type]); + } } return array; @@ -889,6 +1027,39 @@ windows.enumerateWindows = function (callback) { gpii.windows.user32.EnumWindows(proc, 0); }; +/** + * 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; +}; + +/** + * Posts a message to one or more windows (doesn't wait for the response). + * + * See PostMessage (https://msdn.microsoft.com/library/ms644944). + * + * @param hwnd {Number|Number[]} The recipient window handle(s). + * @param msg {Number} The message to post. + * @param wParam {Number} Message specific data. + * @param lParam {Number} Message specific data. + */ +windows.postMessage = function (hwnd, msg, wParam, lParam) { + fluid.each(fluid.makeArray(hwnd), function (w) { + windows.user32.PostMessageW(w, msg, wParam, lParam); + }); +}; + /** * Determines whether this process is running under WoW64. That is, 32-bit node on 64-bit Windows. * diff --git a/gpii/node_modules/processHandling/processHandling.js b/gpii/node_modules/processHandling/processHandling.js index 8e518e796..8c9cb3eb3 100644 --- a/gpii/node_modules/processHandling/processHandling.js +++ b/gpii/node_modules/processHandling/processHandling.js @@ -22,6 +22,7 @@ var ref = require("ref"); var gpii = fluid.registerNamespace("gpii"); var windows = fluid.registerNamespace("gpii.windows"); +var child_process = require("child_process"); require("../WindowsUtilities/WindowsUtilities.js"); @@ -48,6 +49,52 @@ gpii.windows.killProcessByName = function (filename) { } }; +/** + * Finds a running process with the given pid. + * + * @param pid The process pid to search for, null to match any. + * @return {Object} The process information of matching process, otherwise null. + */ +gpii.windows.findProcessByPid = function (pid) { + 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; + } + + 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 (pid === pEntry.th32ProcessID) { + var processInfo = { + pid: pEntry.th32ProcessID, + ppid: pEntry.th32ParentProcessID, + exeFile: processName + }; + // Only want the first one - return it. + return processInfo; + } + + hRes = windows.kernel32.Process32Next(hSnapShot, pEntry.ref()); + } + } finally { + // Make sure the snapshot is closed. + if (hSnapShot) { + windows.kernel32.CloseHandle(hSnapShot); + } + } + + return null; +}; + /** * Finds a running process with the given name. * @@ -125,12 +172,13 @@ gpii.windows.isProcessRunning = function (proc) { if (isNaN(proc)) { togo = gpii.windows.findProcessByName(proc) !== null; } else { - try { - process.kill(proc, 0); - togo = true; - } catch (e) { - togo = false; - } + // try { + // process.kill(proc, 0); + // togo = true; + // } catch (e) { + // togo = false; + // } + togo = gpii.windows.findProcessByPid(proc) !== null; } return togo; }; @@ -351,6 +399,599 @@ gpii.windows.getProcessPath = function (pid, processHandle) { }; /** +<<<<<<< HEAD + * Starts a process given a path an several options. + * + * @param path {String} The path to the executable to be launch. + * @param options {Object} The options. + * @param options.retries The number of retries, trying to launch the process. + * @param options.pollDelay The number of milliseconds to wait between retries. + * @return {promise} The promise will resolve when the process has been started, + * if after all the retries it hasn't been initiated, it will be rejected. + */ +gpii.windows.startProcessByPath = function (path, options) { + var defaultOptions = { + retries: 1, + pollDelay: 100 + }; + + options = fluid.extend(true, defaultOptions, options); + var promise = fluid.promise(); + + var retries = options.retries; + + var retry = function (retries) { + while (retries > 0) { + var proc = child_process.exec(path); + if (proc.pid) { + promise.resolve(); + return; + } else { + setTimeout(retry, options.pollDelay, retries - 1); + } + } + promise.reject({ + isError: true, + message: "Unable to startProcess '" + path + "'." + }); + }; + + retry(retries); + + return promise; +}; + +/** + * Restarts a process given its name. + * + * @param name {String} The name of the process to be restarted. + * @param options {Object} The options. + * @param options.retries The number of retries for trying to restart the process after + * being closed. + * @param options.pollDelay The number of milliseconds to wait between restart retries. + * @param options.timeout The number of milliseconds to wait for the process to close. + * @return {promise} The promise will resolve when the process have been started again after + * being closed, if the timeout expires, or all the retries are used, it will reject. + */ +gpii.windows.restartProcessByName = function (name, options) { + var promise = fluid.promise(); + + var procPid = gpii.windows.findProcessByName(name, false, false); + if (procPid === null) { + promise.reject({ + isError: true, + message: "restartProcessByName: Not able to find process pid." + }); + return promise; + } + + var path = gpii.windows.getProcessPath(procPid); + if (path === null) { + promise.reject({ + isError: true, + message: "restartProcessByName: Not able to retrieve process path." + }); + return promise; + } + + gpii.windows.closeProcessByName(name, options).then( + function () { + gpii.windows.startProcessByPath( + path, + { + retries: options.retries, + pollDelay: options.pollDelay + } + ).then( + function () { + promise.resolve(); + }, + function (err) { + promise.reject({ + isError: true, + message: "restartProcessByName: " + err + }); + } + ); + }, + function (err) { + promise.reject({ + isError: true, + message: "restartProcessByName: " + err + }); + } + ); + + return promise; +}; + +/** + * Gets the windows that each process owns. + * + * @param pids {Number[]} [Optional] Only return these processes. + * @return {Object} Hash of processes and the array of owned windows. + */ +gpii.windows.getProcessWindows = function (pids) { + var processWindows = {}; + + windows.enumerateWindows(function (hwnd) { + var pid = windows.getWindowProcess(hwnd); + if (!pids || pids.indexOf(pid) > -1) { + if (processWindows[pid]) { + processWindows[pid].push(hwnd); + } else { + processWindows[pid] = [hwnd]; + } + } + }); + return processWindows; +}; + +/** + * Gets an array of processes that can be restarted using the Restart Manager API. + * + * @param pids {Number} [Optional] A list of pids to check against (default is all processes). + */ +gpii.windows.getRestartableProcesses = function (pids) { + pids = pids || windows.findProcessByName(null, true); + + return fluid.transform(pids, function (pid) { + + var hProcess = gpii.windows.kernel32.OpenProcess( + windows.API_constants.PROCESS_VM_READ | windows.API_constants.PROCESS_QUERY_INFORMATION, 0, pid); + + var restartable; + if (hProcess) { + var chSize = ref.alloc(windows.types.DWORD); + chSize.fill(0); + restartable = !windows.kernel32.GetApplicationRestartSettings(hProcess, 0, chSize, 0); + } + + var process = null; + if (restartable) { + // Get the create time of each process. The pid and process start time is used to uniquely identify a process + // (considering pid re-use). + 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()); + process = success && {pid: pid, time: creation}; + } + + return process; + + }).filter(fluid.identity); +}; + +/** + * Restart restartable processes. + * + * A process is deemed as restartable if it's explicitly stated it's restartable, by calling RegisterApplicationRestart + * (https://msdn.microsoft.com/library/aa373347). This check is performed in getRestartableProcesses. + * + * @param pids {Number[]} [Optional] The processes to try to restart. Default is all. + * @param options {Object} [Optional] + * @param options.updatePids {Boolean} If true, remove elements from the pids argument that will be restarted. + * @param options.shutdownOnly {Boolean} True to only shutdown and don't restart (applications may not expect this). + * @param options.sessionId {Number} If not undefined, this will be set to the Restart Manager session ID upon return. + * @return {Promise} A promise that will resolve when it's done. + */ +gpii.windows.restartProcesses = function (pids, options) { + options = options || {}; + var updatePids = pids && options.updatePids; + pids = pids || windows.findProcessByName(null, true); + + // Only interested in the processes that can be restarted, otherwise RmShutdown will pause for 30 seconds then kill + // them abruptly. + var restartableProcesses = gpii.windows.getRestartableProcesses(pids); + + // Put them into a RM_UNIQUE_PROCESS array. + var processArray = new windows.RM_UNIQUE_PROCESSES(restartableProcesses.length); + var index = 0; + fluid.each(restartableProcesses, function (process) { + processArray[index].dwProcessId = process.pid; + processArray[index].ProcessStartTime = process.time; + index++; + + if (updatePids) { + var n = pids.indexOf(process.pid); + if (n > -1) { + pids.splice(n, 1); + } + } + }); + + var promise = fluid.promise(); + var ret; + var rm = windows.restartManager; + + // RestartManager.h: + var CCH_RM_SESSION_KEY = 16 * 2 + 1; // sizeof(GUID) * 2 + 1 + + // Wrapper to reject if there's an error. + var reject = function (func, ret) { + return promise.reject({ + isError: true, + message: func + " returned " + ret + ", win32 error:" + windows.kernel32.GetLastError() + }); + }; + + // Create the "restart session". + var session = null; + var sessionBuf = ref.alloc(gpii.windows.types.DWORD); + var sessionKey = new Buffer(CCH_RM_SESSION_KEY * 2); + sessionKey.fill(0); + + ret = rm.RmStartSession(sessionBuf, 0, sessionKey); + if (ret) { + return reject("RmStartSession", ret); + } + + session = sessionBuf.deref(); + if (options.sessionId !== undefined) { + options.sessionId = session; + } + + // Close the session at the end. + promise.then(function () { + rm.RmEndSession(session); + }, function () { + rm.RmEndSession(session); + }); + + // Add the processes to restart. + ret = rm.RmRegisterResources(session, 0, ref.NULL, processArray.length, processArray, 0, ref.NULL); + if (ret) { + return reject("RmRegisterResources", ret); + } + + // Close the processes. This waits for them to shutdown nicely, so it takes a few seconds. + var RmShutdownOnlyRegistered = 10; + rm.RmShutdown.async(session, RmShutdownOnlyRegistered, ref.NULL, function (err, result) { + if (result) { + reject("RmShutdown", result); + return gpii.windows.restartProcesses(pids, options); + } else { + if (!options.shutdownOnly) { + // Re-start the processes again. + var ret = rm.RmRestart(session, 0, ref.NULL); + if (ret) { + return reject("RmRestart", ret); + } + } + promise.resolve(); + } + }); + + return promise; +}; + +/** + * Close all GUI applications (gracefully), and restarts those that are able to be restarted. + * A GUI application is a process that owns a Window. + * + * The purpose of this is to simulate a logout/in, when a change to the system (such as the language) requires all + * applications to restart. + * + * These are the levels that indicate how aggressively applications are closed: + * + * low: Only the applications that have registered themselves as restartable (see restartProcesses()). These are then + * restarted. + * medium: Also notify other windows that a shutdown/logout is imminent (sends WM_QUERYENDSESSION then WM_ENDSESSION). + * They have the option to close themselves. "Any data should be saved automatically without prompting the user" + * high: For the windows that didn't respond to the "medium" level, send WM_QUIT. Most applications will close, or ask + * the user to save changes. (see closeProcessByName()) + * + * @param level {String} Level of force to use (low, medium, or high). + * @param options {Object} [Optional] + * @param options.restart {boolean} True to attempt to restart the restartable applications. + * @param options.pids {Number[]} Only affect these processes. + * @param options.quitDelay {Number} How long (ms) to send WM_QUIT after sending WM_ENDSESSION (default: 3000). + * @return {Promise} Resolves when the close requests have been made (but not acted upon). + */ +gpii.windows.closeApplications = function (level, options) { + options = options || {}; + var levels = { + low: 1, + medium: 2, + high: 3 + }; + var levelNum = levels[level] || levels.low; + + var promise = fluid.promise(); + + // Get the processes and the windows they own. + var processWindows = windows.getProcessWindows(options.pids); + + // Make sure this process doesn't get affected (that would be embarrassing). + // TODO: Need to detect other GPII-related processes. + delete processWindows[process.pid]; + + // Restart applications that have it implemented. + var pids = fluid.transform(fluid.keys(processWindows), function (key) { return parseInt(key); }); + if (levelNum >= levels.low) { + // The restarting processes will be removed from the pids array. + gpii.windows.restartProcesses(pids, {updatePids: true, shutdownOnly: !options.restart}); + } + + var hwnds = []; + + // Pretend the Windows session is about to end. + if (levelNum >= levels.medium) { + // Get the Windows of the processes that couldn't be restarted. + fluid.each(pids, function (pid) { + hwnds.push.apply(hwnds, processWindows[pid]); + }); + + // Send the WM_QUERYENDSESSION then WM_ENDSESSION messages to each window. + windows.postMessage( + hwnds, windows.API_constants.WM_QUERYENDSESSION, 0, windows.API_constants.ENDSESSION_CLOSEAPP); + windows.postMessage( + hwnds, windows.API_constants.WM_ENDSESSION, true, windows.API_constants.ENDSESSION_CLOSEAPP); + } + + if (levelNum >= levels.high) { + // Send WM_QUIT to any Window that's still running, after a delay to give them a chance to process the previous + // messages. + setTimeout(function () { + windows.postMessage(hwnds, windows.API_constants.WM_QUIT, 0, 0); + promise.resolve(); + }, options.quitDelay || 3000); + } else { + promise.resolve(); + } + + return promise; +}; + +/** + * Convenience function for waiting for multiple processes pids. + * + * @param pids {[Number]} The process pids to wait for termination. + * @param options {Object} The options. + * @param options.timeout Aproximate milliseconds to wait for each process. + * @param options.pollDelay How long to wait between processes check. + * @return {promise} The promise will resolve when all the processes have been closed + * or when the timeout expires for any of them. + */ +gpii.windows.waitForProcessesTermination = function (pids, options) { + var defaultOptions = { + timeout: 15000, + pollDelay: 500 + }; + + options = fluid.extend(true, defaultOptions, options); + + var promise = fluid.promise(); + var procsClosed = []; + + for (var i = 0; i < pids.length; i++) { + procsClosed.push(gpii.windows.waitForProcessTermination(pids[i], options)); + } + + fluid.promise.sequence(procsClosed) + .then( + function () { + promise.resolve(); + }, + function () { + promise.reject({ + isError: true, + message: "gpii.windows.waitForProcessesTermination: Timeout, one of the processes didn't close" + }); + } + ); + + return promise; +}; + +/** + * Convenience function for trying to close an application by name, with the minimal necessary level of force. + * + * @param processToClose {String} The name of the App to close. + * @param options {Object} Optional. + * @param options.restart {boolean} True to attempt to restart the restartable applications. + * @param options.quitDelay {Number} How long (ms) to send WM_QUIT after sending WM_ENDSESSION (default: 3000). + * @param options.timeout {Number} How long (ms) to wait before trying to close the application with the superior + * force level. + * @return {promise} The promise will resolve when the application process has been closed, or will reject when + * all the levels of force has failed to close the application. + */ +gpii.windows.closeAppWithMinForce = function (processToClose, options) { + var defaultOptions = { + timeout: 5000, + quitDelay: 3000, + restart: false + }; + + options = fluid.extend(defaultOptions, options); + + var promise = fluid.promise(); + var procPids = gpii.windows.findProcessByName(processToClose, true); + + var tryCloseWithHighForce = function () { + gpii.windows.closeApplications("high", {pids: procPids, restart: options.restart, quitDelay: options.quitDelay}) + .then( + function () { + gpii.windows.waitForProcessesTermination(procPids, options) + .then( + function () { + promise.resolve(); + }, + function (err) { + promise.reject({ + isError: true, + message: "CloseAppWithMinForce: Failed to close process with maximum level of force.", + error: err + }); + } + ); + }, + function (err) { + promise.reject({ + isError: true, + message: "CloseAppWithMinForce: Failed to close process with maximum level of force.", + error: err + }); + } + ); + }; + + var tryCloseWithMediumForce = function () { + gpii.windows.closeApplications("medium", {pids: procPids, restart: options.restart, quitDelay: options.quitDelay}) + .then( + function () { + gpii.windows.waitForProcessesTermination(procPids, options) + .then( + function () { + promise.resolve(); + }, + function () { + tryCloseWithHighForce(); + } + ); + }, + function () { + tryCloseWithHighForce(); + } + ); + }; +// gpii.windows.closeApplications("low", {pids: procPids, restart: options.restart, quitDelay: options.quitDelay}) + gpii.windows.restartProcesses(procPids, {updatePids: true, shutdownOnly: !options.restart}) + .then( + function () { + gpii.windows.waitForProcessesTermination(procPids, options) + .then( + function () { + promise.resolve(); + }, + function () { + tryCloseWithMediumForce(); + } + ); + }, + function () { + tryCloseWithMediumForce(); + } + ); + + return promise; +}; + +/** + * Executed a command that should open or close a process, optionally waits for the change in + * the state of that process, after the state changes, closes another process. + * + * This function si convenient for cases when two applications are tied, for example when a client application + * should be restarted after the underlying service has been stopped. + * + * @param command The command to be executed for closing/opening the application. + * @param args {array} The arguments that are going to be passed to the command. + * @param processToClose {String} The process for closing after the processToWait has exited. + * @param options {Object} Optional. + * @param options.commandTimeout {Number} How long to wait for command execution to have impact on the 'processToWait' + * application, in milliseconds. + * @param options.timeout {Number} How long to wait for the 'processToClose' to die, in milliseconds. + * @param options.pollDelay {Number} Polling delay for all process state change checks in the function, in milliseconds. + * @param options.restart {Bool} Restart the processToClose instead of just closing it. + * @param options.retries {Number} The number of retries for executing the command trying to close/open the processToWait. + * @param options.processToWait {String} The process that is going to be closed/oppened after the command execution. + * @param options.processToWaitState {String} Specificies if command is supposed to "Open" or "Close" the application. + * @return {promise} The will resolve when the 'processToWait' has been openned/closed and the 'processToClose' + * has been sucessfully closed. + */ +gpii.windows.execWaitAndClose = function (command, args, processToClose, options) { + var defaultOptions = { + timeout: 5000, + commandTimeout: 5000, + pollDelay: 100, + restart: false, + retries: 1 + }; + + options = fluid.extend(defaultOptions, options); + + var promise = fluid.promise(); + + // Check if the 'processToWaitState' that is going to be executed is valid. + var validCommands = ["Open", "Close"]; + + if (validCommands.indexOf(options.processToWaitState) === -1) { + promise.reject({ + isError: true, + message: "gpii.windows.execWaitAndClose: Error invalid processToWaitState option '" + options.processToWaitState + "'." + }); + return promise; + } + + var keepTrying = function (rPromise, retries) { + fluid.log("gpii.windows.execWaitAndClose: Retrying command '" + command + "' with args '" + args + "'"); + if (retries < 0) { + if (options.processToWaitState === "Close") { + fluid.log("gpii.windows.execWaitAndClose: Timeout for process closing failed after '" + retries + "' retries."); + rPromise.reject({ + isError: true, + message: "Exceeded the number of retries trying to close process '" + options.processToWait + "'." + }); + } else if (options.processToWaitState === "Open") { + fluid.log("gpii.windows.execWaitAndClose: Timeout for process openning failed after '" + retries + "' retries."); + rPromise.reject({ + isError: true, + message: "Exceeded the number of retries trying to open process '" + options.processToWait + "'." + }); + } + } else { + var cmdRes = gpii.launch.spawn(command, args); + + var closeAppWithMinForce = function (p, process) { + gpii.windows.closeAppWithMinForce( + process, {restart: options.restart, timeout: 1000} ) + .then( + function () { + p.resolve(); + }, + function () { + p.reject({ + isError: true, + message: "gpii.windows.execWaitAndClose: Unable to close application '" + process + }); + } + ); + }; + + var waitForProcessState = options.processToWaitState === "Open" ? + gpii.windows.waitForProcessStart : gpii.windows.waitForProcessTermination; + + waitForProcessState( + options.processToWait, {timeout: options.commandTimeout, pollDelay: options.pollDelay}) + .then( + function () { + closeAppWithMinForce(rPromise, processToClose); + }, + function () { + keepTrying(rPromise, retries - 1); + } + ); + + cmdRes.on("error", function (error) { + rPromise.reject({ + isError: true, + message: "Error while executing command '" + command + "' with args '" + args + "'.", + error: error + }); + }); + } + }; + + keepTrying(promise, options.retries); + + return promise; +}; + +/* * Determines the current state of a service, which can be one of paused, running, or stopped: * * @@ -404,6 +1045,13 @@ gpii.windows.getServiceState = function (serviceName) { return state; }; +fluid.defaults("gpii.windows.closeApplications", { + gradeNames: "fluid.function", + argumentMap: { + "level": 0, + "options": 1 + } +}); fluid.defaults("gpii.windows.killProcessByName", { gradeNames: "fluid.function", @@ -419,3 +1067,13 @@ fluid.defaults("gpii.windows.closeProcessByName", { options: 1 } }); + +fluid.defaults("gpii.windows.execWaitAndClose", { + gradeNames: "fluid.function", + argumentMap: { + command: 0, + args: 1, + processToClose: 2, + options: 3 + } +}); diff --git a/gpii/node_modules/processHandling/test/test-window.c b/gpii/node_modules/processHandling/test/test-window.c index f0f98acd9..13970cddf 100644 --- a/gpii/node_modules/processHandling/test/test-window.c +++ b/gpii/node_modules/processHandling/test/test-window.c @@ -39,7 +39,7 @@ int window() { #define TIMER_ID 2 SetTimer(wnd, TIMER_ID, 60000, NULL); - puts("test-window: Window created"); + printf("test-window: Window created (hwnd:%u, pid:%u)\n", (unsigned int)wnd, GetCurrentProcessId()); // Flush the output - the unit test is waiting for this text. fflush(stdout); @@ -55,13 +55,27 @@ int window() { TranslateMessage(&msg); DispatchMessage(&msg); - if (msg.message == WM_TIMER && msg.wParam == TIMER_ID) { - puts("test-window: Timeout"); - KillTimer(wnd, TIMER_ID); - PostQuitMessage(0); + if (msg.hwnd == wnd) { + switch (msg.message) { + case WM_TIMER: + if (msg.wParam == TIMER_ID) { + puts("test-window: Timeout"); + KillTimer(wnd, TIMER_ID); + PostQuitMessage(0); + } + break; + case WM_QUERYENDSESSION: + puts("message: WM_QUERYENDSESSION"); + break; + case WM_ENDSESSION: + puts("message: WM_ENDSESSION"); + break; + } } + fflush(stdout); } + puts("message: WM_QUIT"); puts("test-window: Ended"); return msg.wParam; } @@ -73,7 +87,7 @@ int main(int argc, char **argv) } puts("A simple application that creates a window that lasts 60 seconds.\n"); - puts(" -window Create a Window."); + puts(" -window Create a Window."); return 0; } diff --git a/gpii/node_modules/processHandling/test/testProcessHandling.js b/gpii/node_modules/processHandling/test/testProcessHandling.js index d24147f6a..95f9a9654 100644 --- a/gpii/node_modules/processHandling/test/testProcessHandling.js +++ b/gpii/node_modules/processHandling/test/testProcessHandling.js @@ -24,12 +24,17 @@ 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; +gpii.tests.windows.processHandling.teardowns = []; jqUnit.module("gpii.tests.windows.processHandling", { setup: function () { @@ -39,6 +44,9 @@ jqUnit.module("gpii.tests.windows.processHandling", { shelljs.cp(path.join(process.env.SystemRoot, "/System32/waitfor.exe"), waitExePath); }, teardown: function () { + while (gpii.tests.windows.processHandling.teardowns.length) { + gpii.tests.windows.processHandling.teardowns.pop()(); + } if (waitExePath !== null) { gpii.windows.killProcessByName(waitExe); shelljs.rm(waitExePath); @@ -46,6 +54,42 @@ jqUnit.module("gpii.tests.windows.processHandling", { } }); +/** + * Starts test-window.exe, returning a promise that resolves with an object containing the window handle, pid, and + * the ChildProcess returned from cp.spawn. + * @param {Array} output {Optional} Empty array to contain the output lines. + * @return {Promise} Resolves with {hwnd, pid, child} when ready. + */ +gpii.tests.windows.processHandling.startTestProcess = function (output) { + var promise = fluid.promise(); + output = output || []; + + // Starts a process which is known to create a window. + var exeName = "test-window.exe"; + var exePath = path.join(__dirname, exeName); + var args = " -window"; + + fluid.log("Executing ", exePath, args); + var child = child_process.exec(exePath + args); + + child.stdout.on("data", function (buf) { + var data = buf.toString(); + output.push.apply(output, data.split(/[\r\n]+/)); + // Wait for the window to be created + var match = data.match(/Window created.*\bhwnd:([0-9]+).*\bpid:([0-9]+)/); + if (match) { + var value = { + hwnd: parseInt(match[1]), + pid: parseInt(match[2]), + child: child + }; + promise.resolve(value); + } + }); + return promise; +}; + + jqUnit.test("Testing findProcessByName", function () { // Find a process that isn't running. var pid = gpii.windows.findProcessByName("a non-matching process.exe"); @@ -65,6 +109,12 @@ jqUnit.test("Testing findProcessByName", function () { // 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); + + // Find all processes. + var allPids = gpii.windows.findProcessByName(null, true); + jqUnit.assertTrue("Should be several running processes.", allPids.length > 1); + jqUnit.assertTrue("Should be more processes than svchost.", allPids.length > pids.length); + }); jqUnit.test("Testing isProcessRunning", function () { @@ -271,6 +321,466 @@ jqUnit.asyncTest("Testing getProcessPath", function () { child.on("close", jqUnit.start); }); +jqUnit.asyncTest("Test getProcessWindows", function () { + + gpii.tests.windows.processHandling.startTestProcess().then(function (child) { + + var runningPids = gpii.windows.findProcessByName(null, true); + var processWindows = gpii.windows.getProcessWindows(); + + jqUnit.assertNotNull("processWindows should not be null", processWindows); + jqUnit.assertNotEquals("processWindows should return something", 0, processWindows.length); + + var pids = fluid.keys(processWindows); + fluid.each(pids, function (pid) { + pid = parseInt(pid); + jqUnit.assertFalse("processWindows should only contain numeric keys", isNaN(pid)); + + var isRunning = runningPids.indexOf(pid) > -1; + jqUnit.assertTrue("processWindows should only contain running processes", isRunning); + }); + + // Assumes this node process doesn't create a window. + jqUnit.assertFalse("processWindows should only return processes with a window", + processWindows.hasOwnProperty(process.pid)); + + var testProcess = processWindows[child.pid]; + jqUnit.assertTrue("processWindows should contain the test process", !!testProcess); + jqUnit.assertTrue("The test process should have a window", testProcess.length > 0); + jqUnit.assertEquals("The test process window should be returned", child.hwnd, testProcess[0]); + + child.child.kill(); + jqUnit.start(); + }); +}); + +jqUnit.test("Test getRestartableProcesses", function () { + + var procs = gpii.windows.getRestartableProcesses(); + + // Find windows explorer (at least one is restartable) + var explorerPids = gpii.windows.findProcessByName("explorer.exe", true); + var found = fluid.find(procs,function (p) { + if (explorerPids.indexOf(p.pid) > -1) { + return true; + } + }); + + jqUnit.assertTrue("Explorer should be restartable", found); + + // Don't find this process + found = fluid.find(procs,function (p) { + return p.pid === process.pid; + }); + + jqUnit.assertFalse("This process should not be restartable", found); +}); + +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 sessionHandle {Number} The session handle. + * @return {boolean} true if it has. + */ +gpii.tests.windows.processHandling.checkRestartManager = function (stage, sessionHandle, pids) { + jqUnit.expect(6); + + 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, restart) { + jqUnit.expect(4); + if (restart) { + jqUnit.expect(3); + } + + expectedPids = fluid.copy(expectedPids); + + var rmShutdown = gpii.windows.restartManager.RmShutdown.async; + var rmRestart = gpii.windows.restartManager.RmRestart; + gpii.tests.windows.processHandling.teardowns.push(function () { + gpii.windows.restartManager.RmShutdown.async = 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) { + if (restart) { + 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); + + gpii.tests.windows.processHandling.checkRestartManager("RmRestart", dwSessionHandle, expectedPids); + } else { + jqUnit.fail("Call to RmRestart was not expected."); + } + }; +}; + +jqUnit.asyncTest("Test restartProcesses", function () { + jqUnit.expect(2); + + // Windows explorer does register as restartable, but restarting it (and potentially failing, because it's a test) + // doesn't feel right. Adding /s to explorer.exe opens an explorer window in a separate process however that process + // doesn't register itself as restartable. + // An attempt was made to make test-window.c register for auto-restart, but for some unknown reason the the call to + // RmShutdown timed out when the process was started by this one (via spawn or exec). + // So, the RmShutdown and RmRestart functions are mocked to check they're being called correctly. + + var exeFile = "explorer.exe"; + var pids = gpii.windows.findProcessByName(exeFile, true); + var explorerPids = fluid.transform(gpii.windows.getRestartableProcesses(pids), function (proc) { + return proc.pid; + }); + + gpii.tests.windows.processHandling.mockRestartManager(explorerPids, true); + + var promise = gpii.windows.restartProcesses(explorerPids); + jqUnit.assertTrue("restartProcesses must return a promise", fluid.isPromise(promise)); + + promise.then(function () { + jqUnit.assert("restartProcesses promise resolved"); + jqUnit.start(); + }, function (e) { + fluid.log(e); + jqUnit.fail("restartProcesses promise rejected"); + }); +}); + +jqUnit.asyncTest("Test closeApplications", function () { + + // Get the restartable explorer process(es) + var explorerPids = gpii.windows.findProcessByName("explorer.exe", true); + explorerPids = fluid.transform(gpii.windows.getRestartableProcesses(explorerPids), function (proc) { + return proc.pid; + }); + + var tests = [ + { + level: "low", + restart: true, + expectedMessages: [] + }, + { + level: "low", + restart: false, + expectedMessages: [] + }, + { + level: "medium", + restart: true, + expectedMessages: [ + "WM_QUERYENDSESSION", + "WM_ENDSESSION" + ] + }, + { + level: "medium", + restart: false, + expectedMessages: [ + "WM_QUERYENDSESSION", + "WM_ENDSESSION" + ] + }, + { + level: "high", + restart: true, + expectedMessages: [ + // medium: + "WM_QUERYENDSESSION", + "WM_ENDSESSION", + // high: + "WM_QUIT" + ] + }, + { + level: "high", + restart: false, + expectedMessages: [ + // medium: + "WM_QUERYENDSESSION", + "WM_ENDSESSION", + // high: + "WM_QUIT" + ] + } + ]; + + jqUnit.expect(tests.length); + + var currentTest; + var runTest = function (testIndex) { + if (testIndex >= tests.length) { + jqUnit.start(); + return; + } + + currentTest = tests[testIndex]; + var output = []; + gpii.tests.windows.processHandling.startTestProcess(output).then(function (child) { + child.child.on("exit", function () { + var messages = fluid.transform(output, function (line) { + var m = line.match(/^message: (WM_((QUERY)?ENDSESSION|QUIT))$/); + return m && m[1]; + }).filter(fluid.identity); + + jqUnit.assertDeepEq("test: " + currentTest.level + ": Expected messages must be received", + currentTest.expectedMessages, messages); + + runTest(testIndex + 1); + }); + + // Expect only explorer to be restarted by restart manager. + gpii.tests.windows.processHandling.mockRestartManager(explorerPids, currentTest.restart); + + var restartPids = [child.pid]; + restartPids.push.apply(restartPids, explorerPids); + + gpii.windows.closeApplications(currentTest.level, { + pids: restartPids, quitDelay: 1, restart: currentTest.restart + }).then(function () { + setTimeout(function () { + child.child.kill(); + }, 500); + }, jqUnit.fail); + + }, jqUnit.fail); + + }; + + runTest(0); +}); + +jqUnit.asyncTest("Testing execWaitAndClose", function () { + jqUnit.expect(1); + + gpii.tests.windows.processHandling.startTestProcess().then(function () { + gpii.windows.execWaitAndClose( + "cmd.exe", + ["dir"], + "test-window.exe", + { + processToWait: "test-window.exe", + processToWaitState: "Open", + timeout: 1000, + commandTimeout: 1000 + } + ).then( + function () { + jqUnit.assert("execWaitAndClose promise resolved."); + jqUnit.start(); + }, + function (err) { + jqUnit.fail(err); + } + ); + }); +}); + +gpii.tests.windows.processHandling.mockCloseApplications = function () { + + var closeApplications = gpii.windows.closeApplications; + gpii.tests.windows.processHandling.teardowns.push(function () { + gpii.windows.closeApplications = closeApplications; + }); + + gpii.windows.closeApplications = function (level) { + var levels = { + low: 1, + medium: 2, + high: 3 + }; + + var levelNum = levels[level] || levels.low; + + var promise = fluid.promise(); + + if (level === "low") { + jqUnit.assertDeepEq("test: First call should be done with level 0", + levelNum, 1); + } else { + jqUnit.fail("mockCloseApplications: Called with incorrect level '" + level + "'"); + } + + promise.reject({ + isError: true, + message: "gpii.windows.closeApplications: Failed to close application with force level '" + level + "'" + }); + + return promise; + }; +}; + +/** + * Check if all properties of expected are also in subject and are equal, ignoring any extra ones in subject. + * + * A property's value in the expected object can be the expected value, fluid.VALUE to match any value (just check if + * the property exists), or fluid.NO_VALUE to check the property doesn't exist. + * + * @param subject {Object} The object to check against + * @param expected {Object} The object containing the values to check for. + * @param maxDepth {Number} [Optional] How deep to check. + */ +gpii.tests.windows.processHandling.deepMatch = function (subject, expected, maxDepth) { + var match = false; + if (maxDepth < 0) { + return false; + } else if (!maxDepth && maxDepth !== 0) { + maxDepth = fluid.strategyRecursionBailout; + } + + if (!subject) { + return subject === expected; + } + + for (var prop in expected) { + if (expected.hasOwnProperty(prop)) { + var exp = expected[prop]; + if (fluid.isPrimitive(exp)) { + match = subject[prop] === exp; + } else { + match = gpii.tests.metrics.deepMatch(subject[prop], exp, maxDepth - 1); + } + if (!match) { + break; + } + } + } + + return match; +}; + +gpii.tests.windows.processHandling.mockCloseProcessByName = function (nameToCheck, optionsToCheck) { + var closeProcessByName = gpii.windows.closeProcessByName; + + gpii.tests.windows.processHandling.teardowns.push(function () { + gpii.windows.closeProcessByName = closeProcessByName; + }); + + gpii.windows.closeProcessByName = function (name, options) { + var promise = fluid.promise(); + + var equalOptions = gpii.tests.windows.processHandling.deepMatch(options, optionsToCheck); + + if (name === nameToCheck && equalOptions) { + jqUnit.assert("closeProcessByName: Correct arguments."); + promise.resolve(); + } else { + jqUnit.fail("closeProcessByName: Incorrect arguments."); + promise.reject({ + isError: true, + message: "gpii.windows.closeProcessByName: Incorrect arguments." + }); + } + + return promise; + }; +}; + +jqUnit.asyncTest("Testing closeAppWithMinForce", function () { + jqUnit.expect(2); + + // Get the restartable explorer process(es) + var explorerPids = gpii.windows.findProcessByName("explorer.exe", true); + explorerPids = fluid.transform(gpii.windows.getRestartableProcesses(explorerPids), function (proc) { + return proc.pid; + }); + + gpii.tests.windows.processHandling.mockCloseApplications(); + gpii.tests.windows.processHandling.mockCloseProcessByName("explorer.exe", {timeout: 1000, restart: true, quitDelay: 100}); + gpii.windows.closeAppWithMinForce("explorer.exe", {restart: true, quitDelay: 100, timeout: 1000}) + .then( + function () { + jqUnit.start(); + }, + function () { + jqUnit.fail(); + } + ); +}); + jqUnit.test("Testing getServiceState", function () { // Local Session Manager will always be running. var state = gpii.windows.getServiceState("LSM"); diff --git a/gpii/node_modules/spiSettingsHandler/src/SpiChildProcess.js b/gpii/node_modules/spiSettingsHandler/src/SpiChildProcess.js index 5bf31da66..8bda36c11 100644 --- a/gpii/node_modules/spiSettingsHandler/src/SpiChildProcess.js +++ b/gpii/node_modules/spiSettingsHandler/src/SpiChildProcess.js @@ -24,6 +24,7 @@ var ffi = require("ffi"); var pvParamPrimitive = process.env.GPII_SPI_PVPARAM_PRIMITIVE === "1"; var action = process.env.GPII_SPI_ACTION; var uiParam = process.env.GPII_SPI_UIPARAM; +var fWinIni = process.env.GPII_SPI_FWININI; var pvParam, type; if (pvParamPrimitive) { @@ -40,4 +41,4 @@ var user32 = ffi.Library("user32", { ] }); -return user32.SystemParametersInfoW(action, uiParam, pvParam, 0); +return user32.SystemParametersInfoW(action, uiParam, pvParam, fWinIni); diff --git a/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js b/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js index 47ecb7df4..8bc91946e 100644 --- a/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js +++ b/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js @@ -20,8 +20,12 @@ https://github.com/GPII/universal/blob/master/LICENSE.txt var ref = require("ref"); var ffi = require("ffi"); + +var fs = require("fs"); var fluid = require("gpii-universal"); +fluid.module.register("gpii.windows.spiSettingsHandlerModule", __dirname, require); + var gpii = fluid.registerNamespace("gpii"); fluid.registerNamespace("gpii.windows.spi"); @@ -76,20 +80,31 @@ gpii.windows.spi.getCurrentSettings = function (payload) { "pvParam": undefined }; - currentSettings.pvParam = (payload.options.pvParam.type === "array") ? - gpii.windows.bufferToArray(pvParam, payload.options.pvParam.valueType) : - pvParam.deref(); + if (payload.options.pvParam.type === "array") { + if (payload.options.pvParam.valueType === "TCHAR") { + var sBuf = pvParam.toString("utf16le"); + var nullPos = sBuf.indexOf("\0"); + currentSettings.pvParam = sBuf.substring(0, nullPos); + } else { + currentSettings.pvParam = + gpii.windows.bufferToArray(pvParam, payload.options.pvParam.valueType); + } + } else { + currentSettings.pvParam = pvParam.deref(); + } return currentSettings; }; -gpii.windows.spi.systemParametersInfo = function (pvParamType, action, uiParam, pvParam) { +gpii.windows.spi.systemParametersInfo = function (pvParamType, action, uiParam, pvParam, fWinIni) { + // Linter Workaround: Linter does not accept default parameters from ES6 spec. + fWinIni = typeof fWinIni !== "undefined" ? fWinIni : 0; var callSuccessful = 0; if (pvParamType === "BOOL" || pvParamType === "UINT") { - callSuccessful = gpii.windows.spi.systemParametersInfoWUint.SystemParametersInfoW(action, uiParam, pvParam, 0); + callSuccessful = gpii.windows.spi.systemParametersInfoWUint.SystemParametersInfoW(action, uiParam, pvParam, fWinIni); } else { - callSuccessful = gpii.windows.spi.systemParametersInfoWPtr.SystemParametersInfoW(action, uiParam, pvParam, 0); + callSuccessful = gpii.windows.spi.systemParametersInfoWPtr.SystemParametersInfoW(action, uiParam, pvParam, fWinIni); } return callSuccessful; @@ -123,7 +138,7 @@ gpii.windows.spi.waitForSpi = function (action) { /** * Performs the SPI call in a child-process. This is used on certain SPI_SET* calls to SPI that are known to be - * troublesome and have the potential to hang. + * troublesome and have the potential to hang, or when calling with fWinIni=SPIF_SENDCHANGE. * * See GPII-2001 for an example, where a system process causes this phenomenon. While it may be possible to fix that * particular issue, it raises the question: what prevents other processes from doing the same? To work around this, @@ -136,9 +151,10 @@ gpii.windows.spi.waitForSpi = function (action) { * @param action The uiAction SPI parameter. * @param uiParam The uiParam SPI parameter. * @param pvParam The pvParam SPI parameter. + * @param fWinIni The fWinIni SPI parameter. * @return {promise} Rejects if the SPI call hangs. */ -gpii.windows.spi.callProblematicSpi = function (pvParamType, action, uiParam, pvParam) { +gpii.windows.spi.callProblematicSpi = function (pvParamType, action, uiParam, pvParam, fWinIni) { var cp = require("child_process"); var primitiveType = pvParamType in gpii.windows.types; @@ -148,7 +164,8 @@ gpii.windows.spi.callProblematicSpi = function (pvParamType, action, uiParam, pv GPII_SPI_ACTION: action, GPII_SPI_UIPARAM: uiParam, GPII_SPI_PVPARAM_PRIMITIVE: primitiveType ? 1 : 0, - GPII_SPI_PVPARAM: primitiveType ? pvParam : pvParam.toString("hex") + GPII_SPI_PVPARAM: primitiveType ? pvParam : pvParam.toString("hex"), + GPII_SPI_FWININI: fWinIni }, execArgv: [] }; @@ -181,6 +198,32 @@ gpii.windows.spi.callProblematicSpi = function (pvParamType, action, uiParam, pv return promise; }; +gpii.windows.spi.checkWallpaperPath = function (path) { + if (typeof path !== "string") { + return false; + } + + var isNotDir = false; + try { + isNotDir = !fs.lstatSync(path).isDirectory(); + } catch (e) { + isNotDir = false; + } + + var isJpg = path.endsWith(".jpg"); + var isPng = path.endsWith(".png"); + + return isNotDir && isPng || isJpg; +}; + +gpii.windows.spi.checkWallpaperPayload = function (payload) { + if (fluid.get(payload, "settings.Wallpaper.value") === undefined) { + return false; + } + var path = payload.settings.Wallpaper.value; + + return gpii.windows.spi.checkWallpaperPath(path); +}; /** * Applies the settings stored in the payload, making a call to SystemParametersInfoW with the @@ -190,6 +233,7 @@ gpii.windows.spi.applySettings = function (payload) { var action = gpii.windows.actionConstants[payload.options.setAction]; var uiParam = gpii.windows.spi.getUiParam(payload); var pvParam = gpii.windows.spi.getPvParam(payload); + var fWinIni = gpii.windows.resolveFlags(payload.options.fWinIni, gpii.windows.API_constants.SpiFlags); uiParam = pvParam.uiParam; // this will be updated because it looks bad pvParam = pvParam.pvParam; @@ -198,6 +242,19 @@ gpii.windows.spi.applySettings = function (payload) { var promiseTogo = fluid.promise(); + // We should include a verification step for the whole payload before + // converting the arguments with the 'get**Param' functions. + // if (action === gpii.windows.actionConstants.SPI_SETDESKWALLPAPER) { + // var validPayload = gpii.windows.spi.checkWallpaperPayload(payload); + // if (!validPayload) { + // promiseTogo.reject({ + // isError: true, + // message: "gpii.windows.spi.applySettings: Invalid PvParam payload '" + pvParam + "'" + // }); + // return promiseTogo; + // } + // } + // The SPI_SET* calls that could potentially halt the process. var problematicSpiCalls = [ gpii.windows.actionConstants.SPI_SETNONCLIENTMETRICS @@ -206,18 +263,25 @@ gpii.windows.spi.applySettings = function (payload) { // Wait for sethc.exe to end before making the SPI call gpii.windows.spi.waitForSpi() .then(function () { + // SPIF_SENDCHANGE broadcasts several messages and has the potential to block, so call it in a child + // process. + var sendChange = fWinIni & gpii.windows.API_constants.SpiFlags.SPIF_SENDCHANGE; - if (problematicSpiCalls.indexOf(action) >= 0) { - var p = gpii.windows.spi.callProblematicSpi(pvParamType, action, uiParam, pvParam); + if (sendChange || (problematicSpiCalls.indexOf(action) >= 0)) { + var p = gpii.windows.spi.callProblematicSpi(pvParamType, action, uiParam, pvParam, fWinIni); fluid.promise.follow(p, promiseTogo); } else { - var callSuccessful = gpii.windows.spi.systemParametersInfo(pvParamType, action, uiParam, pvParam); + var callSuccessful = gpii.windows.spi.systemParametersInfo(pvParamType, action, uiParam, pvParam, fWinIni); if (callSuccessful) { fluid.promise.follow(gpii.windows.spi.waitForSpi(action), promiseTogo); } else { + fluid.log("Something get wrong"); var errorCode = gpii.windows.kernel32.GetLastError(); - fluid.fail("SpiSettingsHandler.js: spi.applySettings() failed with error code " + errorCode + "."); + promiseTogo.reject({ + isError: true, + message: "SpiSettingsHandler.js: spi.applySettings() failed with error code " + errorCode + "." + }); } } }); @@ -260,6 +324,96 @@ gpii.windows.spi.populateResults = function (payload, isNewValue, isGetting, res } }; +/** + * @param {String} s Returns true if value is a string, false otherwhise. + */ +function isString(s) { + if (s && typeof s === "object") { + return s.constructor === String; + } + return typeof s === "string"; +} + +/** + * @param {*} s String to count its words. + * @return The number of words in the string parameter. + */ +function countWords(s) { + // Exclude start and end white-space + s = s.replace(/(^\s*)|(\s*$)/gi,""); + // 2 or more space to 1 + s = s.replace(/[ ]{2,}/gi," "); + // exclude newline with a start spacing + s = s.replace(/\n /,"\n"); + + return s.split(" ").length; +}; + +/** + * @param {*} input String to be splitted into words. + */ +function splitInWords(input) { + var result = input.split(/[ ]+/); + if (result[(result.length - 1)] === "") { + result.pop(); + } + + return result; +} + +/** + * @param {String} flags String with the flags to be parsed. + * @return The values of the parsed string. + */ +gpii.windows.spi.parseFWinIniFlags = function (flags) { + if (flags === undefined || !isString(flags)) { + fluid.log("SpiSettingsHandler.js: spi.parseWinIni() got invalid flags param: " + flags + " of type " + typeof flags + "."); + return 0; + } else { + if (countWords(flags) > 1) { + var words = splitInWords(flags); + var pFlags = []; + + while (words.length >= 1) { + var fWord = words.pop(); + + if (fWord !== "|") { + var flag = gpii.windows.fWinIniFlags[fWord]; + + if (flag !== undefined) { + pFlags.push(flag); + } else { + fluid.log("SpiSettingsHandler.js: spi.parseWinIni() got invalid flags param: " + fWord + " of type " + typeof fWord + "."); + return 0; + } + } + } + + // Linter (Bug) Workaround: Linter fails with arrow style lambda with multiple parameters. + var value = pFlags.reduce(function (v1,v2) { + return v1 | v2; + }); + + return value; + } + } +}; + +/** + * Checks and returns the value for the fWinIni parameter of the SystemParametersInfo function. + */ +gpii.windows.spi.getFWinIni = function (payload) { + var fWinIni = payload.options.fWinIni; + var flagValue = gpii.windows.spi.parseFWinIniFlags(fWinIni); + + if (flagValue === undefined) { + fluid.log("SpiSettingsHandler.js: spi.getFWinIni() got unknown fWinIni param value: " + fWinIni + " of type " + typeof fWinIni + "."); + return 0; + } else { + return flagValue; + } +}; + /** * Returns the value for the uiParam parameter of the SystemParametersInfo function from the * payload. @@ -337,7 +491,11 @@ gpii.windows.spi.getPvParam = function (payload) { } if (payload.options.pvParam.type === "array") { - pvParam = gpii.windows.arrayToBuffer(pvParam, payload.options.pvParam.valueType); + if (payload.options.pvParam.valueType === "TCHAR") { + pvParam = systemSettings.pvParam; + } else { + pvParam = gpii.windows.arrayToBuffer(pvParam, payload.options.pvParam.valueType); + } } else if (payload.options.pvParam.type in gpii.windows.types) { pvParam = systemSettings.pvParam; } else if (payload.options.pvParam.type === "struct") { diff --git a/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js b/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js index 40ccd7d35..97e8327c0 100644 --- a/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js +++ b/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js @@ -27,7 +27,17 @@ require("../src/SpiSettingsHandler.js"); jqUnit.module("SpiSettingsHandler Module"); var testPayloads = ["testMouseTrails", "testFilterKeys", "testMouse", "testMouseClickLock", "testLogFont", - "testNonClientMetrics", "testStickyKeys", "testHighContrast", "testHighContrastToEmpty", "testMouseKeys"]; + "testNonClientMetrics", "testStickyKeys", "testHighContrast", "testHighContrastToEmpty", "testMouseKeys", "testWallpaper"]; + +jqUnit.asyncTest("SPISettingsHandler test - Wallpaper path checking", function () { + var value = fluid.module.resolvePath("%gpii.windows.spiSettingsHandlerModule/../test"); + var res = gpii.windows.spi.checkWallpaperPath(value); + jqUnit.assertDeepEq("Assert testWallpaperPathCheck:'" + value + "' is invalid", res, false); + + value = fluid.module.resolvePath("%gpii.windows.spiSettingsHandlerModule/../test/testWallpaper.jpg"); + res = gpii.windows.spi.checkWallpaperPath(value); + jqUnit.assertDeepEq("Assert testWallpaperPathCheck:'" + value + "' is valid", res, true); +}); fluid.each(testPayloads, function (name) { @@ -40,6 +50,10 @@ fluid.each(testPayloads, function (name) { jqUnit.expect(expectedCount); + if (payload.settings.wallpaper !== undefined) { + payload.settings.wallpaper.value = fluid.module.resolvePath(payload.settings.wallpaper.value); + } + var oldSettings = gpii.windows.spi.getImpl(payload); return gpii.windows.spi.setImpl(payload) diff --git a/gpii/node_modules/spiSettingsHandler/test/testWallpaper.jpg b/gpii/node_modules/spiSettingsHandler/test/testWallpaper.jpg new file mode 100644 index 000000000..a61cc741a Binary files /dev/null and b/gpii/node_modules/spiSettingsHandler/test/testWallpaper.jpg differ diff --git a/gpii/node_modules/spiSettingsHandler/test/testWallpaper.json b/gpii/node_modules/spiSettingsHandler/test/testWallpaper.json new file mode 100644 index 000000000..bedf54dd8 --- /dev/null +++ b/gpii/node_modules/spiSettingsHandler/test/testWallpaper.json @@ -0,0 +1,19 @@ +{ + "options": { + "getAction": "SPI_GETDESKWALLPAPER", + "setAction": "SPI_SETDESKWALLPAPER", + "uiParam": 260, + "pvParam": { + "type": "array", + "valueType": "TCHAR", + "length": 260 + }, + "fWinIni": "SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE" + }, + "settings": { + "wallpaper": { + "value": "%gpii.windows.spiSettingsHandlerModule/../test/testWallpaper.jpg", + "path": "pvParam" + } + } +} diff --git a/gpii/node_modules/startMenuLayout/index.js b/gpii/node_modules/startMenuLayout/index.js new file mode 100644 index 000000000..968cc007f --- /dev/null +++ b/gpii/node_modules/startMenuLayout/index.js @@ -0,0 +1,18 @@ +/* + * Windows Start Menu Layout Settings Handler + * + * Copyright 2018 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"; + +require("./startMenuLayout.js"); diff --git a/gpii/node_modules/startMenuLayout/package.json b/gpii/node_modules/startMenuLayout/package.json new file mode 100644 index 000000000..49c6cdf1d --- /dev/null +++ b/gpii/node_modules/startMenuLayout/package.json @@ -0,0 +1,26 @@ +{ + "name": "tablet-mode", + "version": "0.0.1", + "description": "Simple library to turn on/off Windows 10 tablet mode", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/JavierJF/tablet-mode.git" + }, + "keywords": [ + "windows-10", + "tablet", + "mode", + "tablet-mode" + ], + "author": "Javier Jaramago Fernández", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/JavierJF/tablet-mode/issues" + }, + "homepage": "https://github.com/JavierJF/tablet-mode#readme", + "dependencies": { + "edge-ps": "0.1.0-pre", + "nan": "2.9.2" + } +} diff --git a/gpii/node_modules/startMenuLayout/ps/StartLayout.ps1 b/gpii/node_modules/startMenuLayout/ps/StartLayout.ps1 new file mode 100644 index 000000000..94bbc864b --- /dev/null +++ b/gpii/node_modules/startMenuLayout/ps/StartLayout.ps1 @@ -0,0 +1,43 @@ +param ( + [string]$Path = "empty" +) + +############################################################################# +# If Powershell is running the 32-bit version on a 64-bit machine, we +# need to force powershell to run in 64-bit mode . +############################################################################# +if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") { + write-warning "Not running in 64bits, relaunching script in 64 bit mode" + if ($myInvocation.Line) { + &"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile $myInvocation.Line + }else{ + &"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -file "$($myInvocation.InvocationName)" $args + } +exit $lastexitcode +} + +$VerbosePreference = "continue" +$ErrorActionPreference = "Stop" + +function ExportLayout { + Try { + $ParentDir = Split-Path -Parent $Path + if ($ParentDir -And (Test-Path $ParentDir)) { + Export-StartLayout -Path $Path -ErrorAction Stop + $error = "Success" + $msg = """" + } else { + $error = "Error" + $msg = "Invalid 'Path' parameter" + } + } Catch { + $error = "Error" + $msg = $_.Exception.Message + } + + $error + $msg +} +$return = ExportLayout +$return[0] +$return[1] \ No newline at end of file diff --git a/gpii/node_modules/startMenuLayout/startMenuLayout.js b/gpii/node_modules/startMenuLayout/startMenuLayout.js new file mode 100644 index 000000000..a5db2d65c --- /dev/null +++ b/gpii/node_modules/startMenuLayout/startMenuLayout.js @@ -0,0 +1,137 @@ +/* + * Windows Registry Settings Handler + * + * Copyright 2018 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 child_process = require("child_process"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.module.register("gpii.windows.startMenuLayoutHandlerModule", __dirname, require); +fluid.registerNamespace("gpii.windows.startMenuLayoutHandler"); + +gpii.windows.startMenuLayoutHandler.exportLayoutPs = function (file) { + var promise = fluid.promise(); + console.log(__dirname); + var command = + // -Executable- + "powershell.exe" + + // -- args --- + // Prevent logo + " -NoLogo" + + // Bypass execution policy + " -ExecutionPolicy ByPass" + + // Select the script + " -File " + __dirname + "\\ps\\StartLayout.ps1" + + // Parmeter for script + " -Path " + file; + + child_process.exec(command , function (err, stdout ) { // , stderr) { + if (err) { + promise.reject({ + isError: true, + message: err.toString() + }); + return; + } + var outputWords = stdout.split("\r\n"); + var outputType = outputWords[0]; + + if (outputType === "Error") { + promise.reject({ + isError: true, + message: outputWords[1] + }); + } else { + promise.resolve(); + } + }); + + return promise; +}; + +gpii.windows.startMenuLayoutHandler.setImpl = function (payload) { + var promise = fluid.promise(); + + var currentLayout = process.env.LOCALAPPDATA + "\\GPII\\menuLayouts\\CurrentLayout.xml"; + + var baseKey = "HKEY_CURRENT_USER"; + var path = "Software\\Policies\\Microsoft\\Windows\\Explorer"; + var layoutFileKey = "StartLayoutFile"; + var newLayout = payload.settings.FilePath; + var datatype = "REG_SZ"; + + var fixedMenuKey = "LockedStartLayout"; + var fixedMenuKeyDataType = "REG_DWORD"; + var fixedMenuValue = 1; + + var startMenuShellProc = "ShellExperienceHost.exe"; + + var setFixedMenu = function (layout) { + var layoutStatus = gpii.windows.writeRegistryKey(baseKey, path, layoutFileKey, layout, datatype); + var fixedStatus = gpii.windows.writeRegistryKey(baseKey, path, fixedMenuKey, fixedMenuValue, fixedMenuKeyDataType); + + if (layoutStatus.statusCode !== 200 && fixedStatus !== 200) { + promise.reject({ + isError:true, + message: "startMenuLayoutHandler: Failed to set the registry key." + }); + } else { + gpii.windows.closeProcessByName(startMenuShellProc).then( + function () { + promise.resolve({ FilePath: { oldValue: currentLayout, newValue: layout }}); + }, + function (err) { + promise.reject({ + isError: true, + message: err + }); + }); + } + }; + + if (newLayout !== currentLayout) { + gpii.windows.startMenuLayoutHandler.exportLayoutPs(currentLayout).then( + function () { + setFixedMenu(newLayout); + }, + function (err) { + promise.reject({ + isError: true, + message: err + }); + } + ); + } else { + setFixedMenu(currentLayout); + } + + return promise; +}; + +gpii.windows.startMenuLayoutHandler.getImpl = function () { + var currentLayout = process.env.LOCALAPPDATA + "\\GPII\\menuLayouts\\CurrentLayout.xml"; + return { FilePath: { value: currentLayout }}; +}; + +gpii.windows.startMenuLayoutHandler.set = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(gpii.windows.startMenuLayoutHandler.setImpl, payload); +}; + +gpii.windows.startMenuLayoutHandler.get = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(gpii.windows.startMenuLayoutHandler.getImpl, payload); +}; diff --git a/gpii/node_modules/tabletMode/TabletMode.js b/gpii/node_modules/tabletMode/TabletMode.js new file mode 100644 index 000000000..1cf7c6574 --- /dev/null +++ b/gpii/node_modules/tabletMode/TabletMode.js @@ -0,0 +1,45 @@ +/* + * Windows Registry Settings Handler + * + * Copyright 2018 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"), + TabletModeBindings = require("./src/build/Release/TabletMode"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.windows.tabletMode"); + +gpii.windows.tabletMode.setTabletMode = function (mode) { + return TabletModeBindings.setTabletMode(mode); +}; + +gpii.windows.tabletMode.getTabletMode = function () { + return TabletModeBindings.getTabletMode(); +}; + +fluid.defaults("gpii.windows.tabletMode.setTabletMode", { + gradeNames: "fluid.function", + argumentMap: { + "mode": 0 + } +}); + +fluid.defaults("gpii.windows.tabletMode.getTabletMode", { + gradeNames: "fluid.function", + argumentMap: { + } +}); diff --git a/gpii/node_modules/tabletMode/index.js b/gpii/node_modules/tabletMode/index.js new file mode 100644 index 000000000..338d5649b --- /dev/null +++ b/gpii/node_modules/tabletMode/index.js @@ -0,0 +1,18 @@ +/* + * Windows Tablet Mode Settings Handler + * + * Copyright 2018 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"; + +require("./TabletMode.js"); diff --git a/gpii/node_modules/tabletMode/package.json b/gpii/node_modules/tabletMode/package.json new file mode 100644 index 000000000..5fbde4a78 --- /dev/null +++ b/gpii/node_modules/tabletMode/package.json @@ -0,0 +1,20 @@ +{ + "name": "tabletMode", + "description": "Simple library to turn on/off Windows 10 tablet mode", + "version": "0.0.1", + "author": "GPII", + "bugs": "http://wiki.gpii.net/index.php/Main_Page", + "homepage": "http://gpii.net/", + "dependencies": { + "nan": "2.9.2" + }, + "license": "BSD-3-Clause", + "keywords": [ + "windows-10", + "tablet", + "mode", + "tablet-mode" + ], + "repository": "git://github.com/GPII/windows.git", + "main": "index.js" +} diff --git a/gpii/node_modules/tabletMode/src/TabletMode.cpp b/gpii/node_modules/tabletMode/src/TabletMode.cpp new file mode 100644 index 000000000..3a7e8f517 --- /dev/null +++ b/gpii/node_modules/tabletMode/src/TabletMode.cpp @@ -0,0 +1,220 @@ +#include "TabletMode.h" + +#include +#include + +#include + + +error_t checkArguments(int argc, wchar_t* argv[]) { + if (argc != 2) { + return E_FN_INVAL; + } + + int option = _wtoi(argv[1]); + + if (option != 0 && option != 1) { + return E_FN_INVAL; + } + + return E_FN_SUCCESS; +} + +HRESULT getImmersiveShellInterface(IUnknown** intf) { + if (intf == NULL) { + return E_FN_INVAL; + } + + error_t resCode = E_FN_SUCCESS; + CLSID immersiveShell; + + resCode = + CLSIDFromString( + CComBSTR(OLESTR("{C2F03A33-21F5-47FA-B4BB-156362A2F239}")), + &immersiveShell + ); + if (FAILED(resCode)) { + return resCode; + } + + resCode = + CoCreateInstance( + immersiveShell, + NULL, + CLSCTX_SERVER, + __uuidof(IUnknown), + (void**)intf + ); + + return resCode; +} + +HRESULT getTabletModeInterface(IUnknown* shellIntf, void** intf) { + if (intf == NULL) { + return E_FN_INVAL; + } + + error_t resCode = E_FN_SUCCESS; + + IServiceProvider* servProvider = NULL; + resCode = + shellIntf->QueryInterface( + &servProvider + ); + if (FAILED(resCode)) { + return resCode; + } + + GUID immersiveShellCLSID; + GUID immersiveShellIID; + + resCode = + CLSIDFromString( + CComBSTR(OLESTR("{4fda780a-acd2-41f7-b4f2-ebe674c9bf2a}")), + &immersiveShellCLSID + ); + if (FAILED(resCode)) { + return resCode; + } + + resCode = + CLSIDFromString( + CComBSTR(OLESTR("{4fda780a-acd2-41f7-b4f2-ebe674c9bf2a}")), + &immersiveShellIID + ); + if (FAILED(resCode)) { + return resCode; + } + + resCode = servProvider->QueryService( + immersiveShellCLSID, + immersiveShellIID, + intf + ); + + return resCode; +} + +HRESULT releaseInterface(PVOID* intf) { + return ((IUnknown*)intf)->Release(); +} + +HRESULT triggerTabletMode(uint32_t mode) { + HRESULT resCode = E_FN_SUCCESS; + + resCode = CoInitializeEx(NULL, COINIT_MULTITHREADED); + if (FAILED(resCode)) { + return resCode; + } + + IUnknown* immersiveIntf = NULL; + uint64_t tabletIntf = 0; + + resCode = getImmersiveShellInterface(&immersiveIntf); + if (FAILED(resCode)) { + return resCode; + } + + resCode = getTabletModeInterface(immersiveIntf, (void**)&tabletIntf); + if (FAILED(resCode)) { + goto Cleanup; + } + + uint64_t* vptr = *(uint64_t**)tabletIntf; + + DWORD(*setTabletMode)(uint64_t*,DWORD,DWORD) = + (DWORD(*)(uint64_t*,DWORD,DWORD))vptr[4]; + + resCode = setTabletMode((uint64_t*)tabletIntf, mode, 4); + +Cleanup: + if (tabletIntf != 0) { + releaseInterface((LPVOID*)tabletIntf); + } + if (immersiveIntf != NULL) { + releaseInterface((LPVOID*)immersiveIntf); + } + // Free COM resources. + CoUninitialize(); + + return resCode; +} + +HRESULT getTabletMode(DWORD* mode) { + if (mode == NULL) { + return E_FN_INVAL; + } + + HRESULT resCode = E_FN_SUCCESS; + + resCode = CoInitializeEx(NULL, COINIT_MULTITHREADED); + if (FAILED(resCode)) { + return resCode; + } + + IUnknown* immersiveIntf = NULL; + uint64_t tabletIntf = 0; + + resCode = getImmersiveShellInterface(&immersiveIntf); + if (FAILED(resCode)) { + return resCode; + } + + resCode = getTabletModeInterface(immersiveIntf, (void**)&tabletIntf); + if (FAILED(resCode)) { + goto Cleanup; + } + + uint64_t* vptr = *(uint64_t**)tabletIntf; + + DWORD(*getTabletMode)(uint64_t*,DWORD*) = + (DWORD(*)(uint64_t*,DWORD*))vptr[3]; + + resCode = getTabletMode((uint64_t*)tabletIntf, mode); + +Cleanup: + if (tabletIntf != 0) { + releaseInterface((LPVOID*)tabletIntf); + } + if (immersiveIntf != NULL) { + releaseInterface((LPVOID*)immersiveIntf); + } + // Free COM resources. + CoUninitialize(); + + return resCode; +} + +NAN_METHOD(GetTabletMode) { + long resCode = 0; + DWORD mode = -1; + + resCode = getTabletMode(&mode); + + if (mode == -1) { + info.GetReturnValue().Set(resCode); + } else { + info.GetReturnValue().Set((long)mode); + } +} + +NAN_METHOD(SetTabletMode) { + long param = (long)info[0]->ToNumber()->Value(); + long resCode = 0; + + if (param != 0 && param != 1) { + info.GetReturnValue().Set(EINVAL); + } else { + resCode = triggerTabletMode(param); + info.GetReturnValue().Set(resCode); + } +} + +NAN_MODULE_INIT(init) { + Nan::Set(target, Nan::New("setTabletMode").ToLocalChecked(), + Nan::GetFunction(Nan::New(SetTabletMode)).ToLocalChecked()); + Nan::Set(target, Nan::New("getTabletMode").ToLocalChecked(), + Nan::GetFunction(Nan::New(GetTabletMode)).ToLocalChecked()); +} + +NODE_MODULE(TabletMode, init) \ No newline at end of file diff --git a/gpii/node_modules/tabletMode/src/TabletMode.h b/gpii/node_modules/tabletMode/src/TabletMode.h new file mode 100644 index 000000000..ae6c88e84 --- /dev/null +++ b/gpii/node_modules/tabletMode/src/TabletMode.h @@ -0,0 +1,18 @@ +#ifndef __TABLET_MODE_H +#define __TABLET_MODE_H + +#include "uerrno.h" + +#define _WINSOCKAPI_ // stops windows.h including winsock.h +#include + +#include + +error_t checkArguments(int argc, wchar_t* argv[]); + +HRESULT getTabletModeInterface(IUnknown* shellIntf, void** intf); +HRESULT triggerTabletMode(uint32_t mode); +HRESULT getTabletMode(DWORD* mode); +HRESULT releaseInterface(LPVOID* intf); + +#endif diff --git a/gpii/node_modules/tabletMode/src/binding.gyp b/gpii/node_modules/tabletMode/src/binding.gyp new file mode 100644 index 000000000..6bf900c20 --- /dev/null +++ b/gpii/node_modules/tabletMode/src/binding.gyp @@ -0,0 +1,16 @@ +{ + "targets": [ + { + "target_name": "TabletMode", + "sources": [ + "uerrno.h", + "uerrno.cpp", + "TabletMode.h", + "TabletMode.cpp", + ], + "include_dirs" : [ + " + +/** + * C++ includes. + */ +#include + +/** + * @brief + * Copy a *len* number of chars from *src* into *dest*, and then append a null + * character to the last *dest* position. + * @details + * This function always terminates the *dest* buffer with a null character, so + * the buffer should have at least *len + 1* length. + * The function stops copying at the moment of finding a *null* character. + * @param dest + * Buffer that is going to hold the copied characters from src. + * @param src + * Buffer with the characters to be copied. + * @param len + * The numbers of elements to be copied from one string to the other. + * + */ +size_t _strlcpy(char* dest, const char* src, const size_t len) { + if (len == 0 || dest == nullptr || src == nullptr) { + return 0; + } + + size_t i = 0; + + for(i = 0; i < len; i++) { + char cur_char = src[i]; + if (cur_char == '\0') { + break; + } else { + dest[i] = cur_char; + } + } + + dest[i] = '\0'; + + return i; +} + +bool is_fnerr(error_t error) { + if (error > -20 && error <= 0) { + return true; + } else { + return false; + } +} + +errno_t fnerr_to_errstr(fnerr_t errcode, struct errstr* err) { + int64_t n = sizeof(fn_errmap)/sizeof(fn_errmap[0]); + + if (err == nullptr || errcode <= (-n) || errcode > E_FN_SUCCESS) { + return E_FN_INVAL; + } + + for (int i = 0; i < n; ++i) { + if (errcode == fn_errmap[i].code) { + size_t msg_len = strlen(fn_errmap[i].msg); + + if (msg_len > err->size) { + return E_FN_INVAL_ARRSZ; + } + + _strlcpy(err->data, fn_errmap[i].msg, msg_len); + err->offset = msg_len; + } + } + + return E_FN_SUCCESS; +} + +#if (defined _WIN32) + +errno_t winerr_to_errno(DWORD code, errno_t deferrno) { + for (int i = 0; win_errmap[i].w != 0; ++i) { + if (code == win_errmap[i].w) { + return win_errmap[i].e; + } + } + + return deferrno; /* FIXME: what's so special about EACCESS? */ +} + +DWORD syserr_to_errstr(DWORD errCode, struct errstr* msg) { + if (errCode == 0) { + return ERROR_BAD_ARGUMENTS; + } + + DWORD offset = + FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + errCode, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + msg->data, + msg->size, + NULL + ); + + if (offset == 0) { + return offset; + } else { + msg->offset = offset; + } + + return S_OK; +} + +// Returns the last Win32 error, in string format. Returns an empty string if there is no error. +std::string GetLastErrorAsString() { + //Get the error message, if any. + DWORD errorMessageID = ::GetLastError(); + if(errorMessageID == 0) + return std::string(); //No error message has been recorded + + LPSTR messageBuffer = nullptr; + size_t size = + FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + errorMessageID, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&messageBuffer, + 0, + NULL + ); + + std::string message(messageBuffer, size); + + //Free the buffer. + LocalFree(messageBuffer); + + return message; +} + +#elif (defined __linux) + +/** + * This uses the XSI compilant function strerror_r, because it provides + * thread safety and also it returs an error if the buffer size provided + * is too small, "errno=ERANGE". + */ +errno_t syserr_to_errstr(errno_t errCode, struct errstr* msg) { + errno = 0; + + if (errCode == 0) { + return EINVAL; + } + + auto errRes = strerror_r(errCode, msg->data, msg->size); + + if (errRes != 0) { + return errRes; + } else { + auto offset = strlen(msg->data); + msg->offset = offset; + } + + return 0; +} + +#endif diff --git a/gpii/node_modules/tabletMode/src/uerrno.h b/gpii/node_modules/tabletMode/src/uerrno.h new file mode 100644 index 000000000..4979e7f8f --- /dev/null +++ b/gpii/node_modules/tabletMode/src/uerrno.h @@ -0,0 +1,437 @@ +#ifndef U_ERRNO_H +#define U_ERRNO_H + +#if (defined _WIN32) + +#define _WINSOCKAPI_ // stops windows.h including winsock.h +#include + +#include +#include + +#elif (defined __linux) + +#include +#include + +#endif + +extern "C" { + +/** + * @typedef errno_t + * + * @brief + * If a function returns errno, it should be marked with this type as return type. + * + * @details + * In case a function can stick to standard errno messages, then it should have this + * type as a return type. But if the function aims to be crossplaftorm then it's most + * likely not to return this type, due to differences in the POSIX standard implementation. + * Compatibility Note:
+ * This type should be part of the C11 standard, but there are compilers that still + * doesn't support it, this def is just preventive. + */ +#ifndef __STDC_LIB_EXT1__ + typedef int errno_t; +#endif + +/** + * @typedef error_t + * + * @brief + * This type should be used if the function retrieves a user defined error + * or a system error. + * @details + * If a function returns system errors, but it can also return specific user + * defined errors, that cannot be conveniently specified using only POSIX + * complaints error codes, then this error type should be used. + */ +#if (defined _WIN32) +typedef errno_t error_t; +#elif (defined __linux) +typedef errno_t error_t; +#endif + +enum fnerr { + E_FN_SUCCESS = 0, /*!< Function success */ + E_FN_INVAL = -1, /*!< Invalid argument */ + E_FN_INVAL_ARRSZ = -2, /*!< Invalid argument array size */ + E_FN_INVAL_INIT = -3, /*!< Argument with invalid initialization */ + E_FN_DOM = -4, /*!< Argument out of function domain */ + E_FN_RANGE = -5, /*!< Result out of range */ + E_FN_NOSYS = -6, /*!< Function not implemented */ + E_FN_NOTSUP = -7, /*!< Funcitonality not supported */ + E_FN_NOCONV = -8, /*!< Function not converging to result */ + E_FN_ABORTED = -9, /*!< Function execution interrupted or aborted */ + E_FN_OUTVAL = -10 /*!< Invalid result */ +}; + +/** + * @typedef fnerr_t + * + * @brief + * This type should be used in function with only can return errors related with + * internal function logic, and function parameters preconditions satisfaction. + */ +typedef errno_t fnerr_t; + +/** + * @brief Convenience function to check if an error is a fnerr_t. + * @param error Error to check if it's a fnerr_t. + * @return + * True if supplied error is a fnerr_t, false otherwise. + */ +bool is_fnerr(error_t error); + +/** + * @enum g_syserr + * @brief Enum for marking function error code as global variable system setted one. + * @details + * This enum and the syserr_t typedef are useful when you want to point that + * a function is setting a global error variable, with a system error code, like + * 'errno'. Doing so it's more easy to write crossplatform functions and react + * to the returned error code when you don't have time to fully classify errors. + */ +/** + * @var g_syserr::E_GV_SYS_POSIXERROR + * @brief Underlying system error with POSIX code. Need to get errno to retrieve + * the last error. + */ +/** + * @var g_syserr::E_GV_SYS_WIN32ERROR + * @brief Underlying system error with WIN32 code. Need to call GetLastError() to + * retrieve the last error. + */ +/** + * @var g_syserr::E_GV_SYS_MACOSERROR + * @brief Underlying system error. + */ +enum g_syserr { + E_GV_SYS_POSIXERROR = -30, + E_GV_SYS_WIN32ERROR = -31, + E_GV_SYS_MACOSERROR = -32 +}; + +/** + * @typedef g_syserr_t + * + * @brief + * This type should be used in function with can return 'fnerr_t' errors or + * any system related error. + */ +typedef errno_t g_syserr_t; +/** + * @typedef syserr_t + * + * @brief + * This type should be used in function with can return 'fnerr_t' errors or + * any system related error. + */ +typedef errno_t syserr_t; +/** + * @typedef neterr_t + * + * @brief + * This type should be used in function with can return 'fnerr_t' errors or + * system networking related errors. + */ +typedef errno_t neterr_t; + +/** + * @typedef neterr_t + * + * @brief + * This type should be used in function with can return 'fnerr_t' errors or + * system filesystem related errors. + */ +typedef errno_t fserr_t; + +/** + * @typedef neterr_t + * + * @brief + * This type should be used in function with can return 'fnerr_t' errors or + * system devices related errors. + */ +typedef errno_t deverr_t; + +/** + * @typedef errext_t + * + * @brief + * This type should be used as the return type for a function that returns error + * codes directly from a third party library, without further classification or + * mapping. + */ +#if (defined _WIN32) +typedef errno_t exterr_t; +#elif (defined __linux) +typedef errno_t exterr_t; +#endif + +#if (defined __linux) +typedef unsigned long DWORD +#endif + +#if (defined _WIN32) + +/* + * The follwing code is based in the Cygwin OpenSource project errno.c file, licensed + * under Cygwin License, Copyright Red Hat, Inc. + * + * Error codes has been modified for full MSVC compatibility. + */ + +#define X(w, e) {ERROR_##w, #w, e} + +/** + * @brief Struct error map for maping system error codes. + * @details + * Mapping error codes for multiplatform conversion. + */ +static const struct +{ + DWORD w; /* Windows version of error */ + const char *s; /* Text of windows version */ + int e; /* errno version of error */ +} win_errmap[] = +{ + /* FIXME: Some of these choices are arbitrary! */ + X (ACCESS_DENIED, EACCES), + X (ACTIVE_CONNECTIONS, EAGAIN), + X (ALREADY_EXISTS, EEXIST), + X (BAD_DEVICE, ENODEV), + X (BAD_EXE_FORMAT, ENOEXEC), + X (BAD_NETPATH, ENOENT), + X (BAD_NET_NAME, ENOENT), + X (BAD_NET_RESP, ENOSYS), + X (BAD_PATHNAME, ENOENT), + X (BAD_PIPE, EINVAL), + X (BAD_UNIT, ENODEV), + X (BAD_USERNAME, EINVAL), + X (BEGINNING_OF_MEDIA, EIO), + X (BROKEN_PIPE, EPIPE), + X (BUSY, EBUSY), + X (BUS_RESET, EIO), + X (CALL_NOT_IMPLEMENTED, ENOSYS), + X (CANCELLED, EINTR), + X (CANNOT_MAKE, EPERM), + X (CHILD_NOT_COMPLETE, EBUSY), + X (COMMITMENT_LIMIT, EAGAIN), + X (CONNECTION_REFUSED, ECONNREFUSED), + X (CRC, EIO), + X (DEVICE_DOOR_OPEN, EIO), + X (DEVICE_IN_USE, EAGAIN), + X (DEVICE_REQUIRES_CLEANING, EIO), + X (DEV_NOT_EXIST, ENOENT), + X (DIRECTORY, ENOTDIR), + X (DIR_NOT_EMPTY, ENOTEMPTY), + X (DISK_CORRUPT, EIO), + X (DISK_FULL, ENOSPC), + X (DS_GENERIC_ERROR, EIO), +//X (DUP_NAME, ENOTUNIQ), + X (EAS_DIDNT_FIT, ENOSPC), + X (EAS_NOT_SUPPORTED, ENOTSUP), + X (EA_LIST_INCONSISTENT, EINVAL), + X (EA_TABLE_FULL, ENOSPC), + X (END_OF_MEDIA, ENOSPC), + X (EOM_OVERFLOW, EIO), + X (EXE_MACHINE_TYPE_MISMATCH, ENOEXEC), + X (EXE_MARKED_INVALID, ENOEXEC), + X (FILEMARK_DETECTED, EIO), + X (FILENAME_EXCED_RANGE, ENAMETOOLONG), + X (FILE_CORRUPT, EEXIST), + X (FILE_EXISTS, EEXIST), + X (FILE_INVALID, ENXIO), + X (FILE_NOT_FOUND, ENOENT), + X (HANDLE_DISK_FULL, ENOSPC), + X (HANDLE_EOF, ENODATA), + X (INVALID_ADDRESS, EINVAL), + X (INVALID_AT_INTERRUPT_TIME, EINTR), + X (INVALID_BLOCK_LENGTH, EIO), + X (INVALID_DATA, EINVAL), + X (INVALID_DRIVE, ENODEV), + X (INVALID_EA_NAME, EINVAL), + X (INVALID_EXE_SIGNATURE, ENOEXEC), +//X (INVALID_FUNCTION, EBADRQC), + X (INVALID_HANDLE, EBADF), + X (INVALID_NAME, ENOENT), + X (INVALID_PARAMETER, EINVAL), + X (INVALID_SIGNAL_NUMBER, EINVAL), + X (IOPL_NOT_ENABLED, ENOEXEC), + X (IO_DEVICE, EIO), + X (IO_INCOMPLETE, EAGAIN), + X (IO_PENDING, EAGAIN), + X (LOCK_VIOLATION, EBUSY), + X (MAX_THRDS_REACHED, EAGAIN), + X (META_EXPANSION_TOO_LONG, EINVAL), + X (MOD_NOT_FOUND, ENOENT), + X (MORE_DATA, EMSGSIZE), + X (NEGATIVE_SEEK, EINVAL), + X (NETNAME_DELETED, ENOENT), + X (NOACCESS, EFAULT), + X (NONE_MAPPED, EINVAL), + X (NONPAGED_SYSTEM_RESOURCES, EAGAIN), + X (NOT_CONNECTED, ENOLINK), + X (NOT_ENOUGH_MEMORY, ENOMEM), + X (NOT_ENOUGH_QUOTA, EIO), + X (NOT_OWNER, EPERM), +//X (NOT_READY, ENOMEDIUM), + X (NOT_SAME_DEVICE, EXDEV), + X (NOT_SUPPORTED, ENOSYS), + X (NO_DATA, EPIPE), + X (NO_DATA_DETECTED, EIO), +//X (NO_MEDIA_IN_DRIVE, ENOMEDIUM), +//X (NO_MORE_FILES, ENMFILE), +//X (NO_MORE_ITEMS, ENMFILE), + X (NO_MORE_SEARCH_HANDLES, ENFILE), + X (NO_PROC_SLOTS, EAGAIN), + X (NO_SIGNAL_SENT, EIO), + X (NO_SYSTEM_RESOURCES, EFBIG), + X (NO_TOKEN, EINVAL), + X (OPEN_FAILED, EIO), + X (OPEN_FILES, EAGAIN), + X (OUTOFMEMORY, ENOMEM), + X (PAGED_SYSTEM_RESOURCES, EAGAIN), + X (PAGEFILE_QUOTA, EAGAIN), + X (PATH_NOT_FOUND, ENOENT), + X (PIPE_BUSY, EBUSY), + X (PIPE_CONNECTED, EBUSY), +//X (PIPE_LISTENING, ECOMM), +//X (PIPE_NOT_CONNECTED, ECOMM), + X (POSSIBLE_DEADLOCK, EDEADLOCK), + X (PRIVILEGE_NOT_HELD, EPERM), + X (PROCESS_ABORTED, EFAULT), + X (PROC_NOT_FOUND, ESRCH), +//X (REM_NOT_LIST, ENONET), + X (SECTOR_NOT_FOUND, EINVAL), + X (SEEK, EINVAL), + X (SERVICE_REQUEST_TIMEOUT, EBUSY), + X (SETMARK_DETECTED, EIO), + X (SHARING_BUFFER_EXCEEDED, ENOLCK), + X (SHARING_VIOLATION, EBUSY), + X (SIGNAL_PENDING, EBUSY), + X (SIGNAL_REFUSED, EIO), +//X (SXS_CANT_GEN_ACTCTX, ELIBBAD), + X (THREAD_1_INACTIVE, EINVAL), + X (TIMEOUT, EBUSY), + X (TOO_MANY_LINKS, EMLINK), + X (TOO_MANY_OPEN_FILES, EMFILE), + X (UNEXP_NET_ERR, EIO), + X (WAIT_NO_CHILDREN, ECHILD), + X (WORKING_SET_QUOTA, EAGAIN), + X (WRITE_PROTECT, EROFS), + { S_OK, S_OK, 0 }, + { 0, NULL, 0} +}; + +/** + * @brief + * Function that maps win32 errors to errno values. + * @param code + * Win32 error code to be mapped to a errno value. + * @param deferrno + * Default error code to be returned if no matching error code is found for the + * supplied code. + * @return + * A mapping value for the provided error code, or the default error code supplied. + */ +errno_t winerr_to_errno(DWORD code, errno_t deferrno); + +#endif + +/** + * @brief + * Struct for holding error messages represented as an array of chars. + */ +struct errstr { +#if (defined _WIN32) + LPSTR data; +#elif (defined __linux) + char* data; +#endif + size_t size; + size_t offset; +}; + +/** + * String map for fnerr values. + */ +static const struct { + fnerr_t code; + const char* msg; +} fn_errmap[] = { + { E_FN_SUCCESS, "Function success" }, + { E_FN_INVAL, "Invalid input" }, + { E_FN_INVAL_ARRSZ, "Not enough space in array input parameter" }, + { E_FN_INVAL_INIT, "Invalid argument initialization" }, + { E_FN_DOM, "Argument out of function domain" }, + { E_FN_RANGE, "Result out of range" }, + { E_FN_NOSYS, "Function not implemented" }, + { E_FN_NOTSUP, "Functionality not supported" }, + { E_FN_NOCONV, "Function not converging to result" }, + { E_FN_ABORTED, "Aborted function execution" }, + { E_FN_OUTVAL, "Invalid result" } +}; + +static const struct { + g_syserr_t code; + const char* msg; +} g_syserrmap[] = { + { E_GV_SYS_POSIXERROR, "Error from OS, use errno() to retrieve it" }, + { E_GV_SYS_WIN32ERROR, "Error from OS, use GetLastError() to retrieve it" }, + { E_GV_SYS_MACOSERROR, "Error from OS, use ... to retrieve it" } +}; + +/** + * @brief + * Function that returns a error description for a given fnerr_t + * @param errcode Code to be translated to humand readable msg. + * @param msg Struct + * @return + * Errors: + * - E_FN_INVAL: Provied error code was not found. + * - E_FN_INVAL_ARRSZ: Provied buffer is not big enough for holding message error. + */ +errno_t fnerr_to_errstr(fnerr_t errcode, struct errstr* msg); + +#if (defined _WIN32) +/** + * @brief Thread safe function for returning the corresponding string for an error code. + * @param errCode The error code to be translated into a message. + * @param msg The error string buffer to be filled with the message. + * @return + * 0 if success, error code otherwise. If the string buffer provided is + * not big enought for holding the message, a error is returned. + */ +DWORD syserr_to_errstr(DWORD errcode, struct errstr* msg); + +#elif (defined __linux) +/** + * @brief Thread safe function for returning the corresponding string for an error code. + * @param errCode The error code to be translated into a message. + * @param msg The error string buffer to be filled with the message. + * @return + * 0 if success, error code otherwise. If the string buffer provided is + * not big enought for holding the message, a error is returned. + */ +errno_t syserr_to_errstr(errno_t errcode, struct errstr* msg); + +#endif + +/** + * @brief + * Function to be defined by library users, in order to translate their own + * defined errors into human readable error messages. + * @param errcode The error code to be translated into a message. + * @return + * Errors: + * - E_FN_INVAL: Provied error code was not found. + * - E_FN_INVAL_ARRSZ: Provied buffer is not big enough for holding message error. + */ +// errno_t to_errstr(error_t errcode, struct errstr* msg); + +} + +#endif diff --git a/index.js b/index.js index 4d55d0d74..8d48a52d0 100644 --- a/index.js +++ b/index.js @@ -36,5 +36,7 @@ require("./gpii/node_modules/windowsMetrics"); require("./gpii/node_modules/processReporter"); require("./gpii/node_modules/windowMessages"); require("./gpii/node_modules/userListeners"); +require("./gpii/node_modules/tabletMode"); +require("./gpii/node_modules/startMenuLayout"); module.exports = fluid; diff --git a/package.json b/package.json index 0eb848ea5..c40dca5b2 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "ref": "1.3.4", "ref-struct": "1.1.0", "ref-array": "1.1.2", + "ref-wchar": "1.0.2", "edge-js": "8.8.1", "string-argv": "0.0.2", "@pokusew/pcsclite": "0.4.18", - "gpii-universal": "0.3.0-dev.20180314T153442Z.bcc35970" + "grunt-shell": "1.3.0", + "gpii-universal": "JavierJF/universal#GPII-2521" }, "devDependencies": { "grunt": "1.0.2", diff --git a/provisioning/Chocolatey.ps1 b/provisioning/Chocolatey.ps1 index 1e5e00991..6a8d7c02b 100644 --- a/provisioning/Chocolatey.ps1 +++ b/provisioning/Chocolatey.ps1 @@ -33,4 +33,7 @@ refreshenv Invoke-Command $chocolatey "install msbuild.extensionpack -y" refreshenv +Invoke-Expression "$env:SystemDrive\vagrant\provisioning\chocopkgs\ClassicShell.ps1" +Invoke-Expression "$env:SystemDrive\vagrant\provisioning\chocopkgs\AutoHotkey.ps1" + exit 0 diff --git a/provisioning/Installer.ps1 b/provisioning/Installer.ps1 index f7aa5511c..1b836a973 100644 --- a/provisioning/Installer.ps1 +++ b/provisioning/Installer.ps1 @@ -36,6 +36,9 @@ Invoke-Command "robocopy" ".. $($stagingWindowsDir) gpii.js index.js package.jso Invoke-Command $npm "prune --production" $stagingWindowsDir +# Create the dir for holding data for GPII +Invoke-Expression "$env:SystemDrive\vagrant\provisioning\envChanges\CreateDataDir.ps1" + md (Join-Path $installerDir "output") md (Join-Path $installerDir "temp") @@ -43,3 +46,18 @@ Invoke-Environment "C:\Program Files (x86)\Microsoft Visual C++ Build Tools\vcbu $setupDir = Join-Path $installerDir "setup" $msbuild = Get-MSBuild "4.0" Invoke-Command $msbuild "setup.msbuild" $setupDir + +# Install MSVC common tools for VC++ development +$tempDir = Join-Path $installerDir "temp" +$msvcInstaller = Join-Path $tempDir "msvc-inst.exe" +wget "https://download.microsoft.com/download/0/B/C/0BC321A4-013F-479C-84E6-4A2F90B11269/vs_community.exe" -OutFile $msvcInstaller + +# Visual studio parameters +$select = '/InstallSelectableItems' +$quiet = '/quiet' + +# Visual studio components +$ATL = 'NativeLanguageSupport_VCV1' +$ATLMF = 'NativeLanguageSupport_MFCV1' + +Invoke-Command $msvcInstaller "$select `"$ATL;$ATLMF`" $quiet" diff --git a/provisioning/chocopkgs/AutoHotkey.ps1 b/provisioning/chocopkgs/AutoHotkey.ps1 new file mode 100644 index 000000000..2b9e72056 --- /dev/null +++ b/provisioning/chocopkgs/AutoHotkey.ps1 @@ -0,0 +1,25 @@ +<# + This script install everything needed by Win10 Simplification module. + + If the script is copied and run from a temporary folder (like when running via vagrant) + the -originalBuildScriptPath parameter should be passed with the path to the original + "provisioning" folder +#> + +param ( # default to script path if no parameter is given + [string]$originalBuildScriptPath = (Split-Path -parent $PSCommandPath) +) + +if ($originalBuildScriptPath -eq (Split-Path -parent $PSCommandPath)) { + Import-Module "$($originalBuildScriptPath)/../Provisioning.psm1" -Force +} else { + Import-Module "$($originalBuildScriptPath)/Provisioning.psm1" -Force +} + +$chocolatey = "$env:ChocolateyInstall\bin\choco.exe" -f $env:SystemDrive + +# Install AutoHotKey package. +Invoke-Command $chocolatey "install autohotkey --yes --force" +refreshenv + +exit 0 diff --git a/provisioning/chocopkgs/ClassicShell.ps1 b/provisioning/chocopkgs/ClassicShell.ps1 new file mode 100644 index 000000000..1ce173c7a --- /dev/null +++ b/provisioning/chocopkgs/ClassicShell.ps1 @@ -0,0 +1,115 @@ +<# + Script that provision the machine for the Windows 10 morpher. +#> + +Import-Module (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '../Provisioning.psm1') -Force + +$VerbosePreference = "continue" + +$chocolatey = "$env:ChocolateyInstall\bin\choco.exe" -f $env:SystemDrive +$classicShellVers = "4.3.0.0" +$classicShellDir = "C:\Program Files\Classic Shell\" +$classicShellPath = "C:\Program Files\Classic Shell\ClassicStartMenu.exe" +$classicShellProcessName = "ClassicStartMenu" +$classicShellStopScript = Join-Path $(Split-Path -Parent $MyInvocation.MyCommand.Path) "ClassicShellStop.ps1" + +function installClassicShell() +{ + try + { + <# Classic-Shell installation #> + Invoke-Command $chocolatey "install classic-shell --version $($classicShellVers) -y" "" 0 + } + catch + { + <# We should notify that IoD have fail #> + $ErrorMessage = $_.Except0on.Message + Write-Verbose "$ErrorMessage" + } +} + +function selectVal($regPath, $val) +{ + if ($val.type -eq "REG_DWORD") { + New-ItemProperty -Path $regpath -Name $val.name -Value $val.value ` + -PropertyType DWORD -Force | Out-Null + } elseif ($val.type -eq "REG_SZ") { + New-ItemProperty -Path $regpath -Name $val.name -Value $val.value ` + -PropertyType String -Force | Out-Null + } else { + Write-Error ("Unvalid type: " + $val.type) + } +} + +function createNewKey($regPath, $vals) { + if(!(Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + + if ($vals -ne $null) { + foreach($val in $vals) { + selectVal $regPath $val + } + } +} + +function setClassicShellRegistryKeys() { + $registryPath = "HKCU:\Software\IvoSoft" + $Name = "Version" + $value = "1" + + # Disable first run menu selection + $settingsPath = "$registryPath\ClassicStartMenu" + $vals = @((newVal "ShowedStyle2" "REG_DWORD" 1)) + + createNewKey $settingsPath $vals + + $settingsPath = "$registryPath\ClassicStartMenu\Settings" + $vals = @((newVal "EnableStartButton" "REG_DWORD" 1), + (newVal "StartButtonType" "REG_SZ" "CustomButton"), + (newVal "StartButtonPath" "REG_SZ" "$env:LOCALAPPDATA\GPII\logo\win-logo.png"), + (newVal "StartButtonSize" "REG_DWORD" 42), + (newVal "SkipMetro" "REG_DWORD" 1), + (newVal "MenuStyle" "REG_SZ" "Win7"), + (newVal "SkinW7" "REG_SZ" "Windows Aero"), + (newVal "AutoStart" "REG_DWORD" 0)) + + createNewKey $settingsPath $vals +} + +function newVal($name, $type, $val) +{ + $keyobj = New-Object -TypeName PSObject + $keyobj | Add-Member -MemberType NoteProperty -Name name -Value $name + $keyobj | Add-Member -MemberType NoteProperty -Name type -Value $type + $keyobj | Add-Member -MemberType NoteProperty -Name value -Value $val + + return $keyobj +} + +function stopClassicShell() { + try + { + <# Currently there is no way of being sure classicShell has exit #> + $process = Get-Process $ClassicShellProcessName -ea SilentlyContinue + + if ($process) + { + Invoke-Command $classicShellPath "-exit" "" 0 + Write-Verbose "Classic-Shell stopped" + } + } + catch + { + <# ($_.FullyQualifiedErrorId).split(',')[0] #> + Write-Verbose ("Failed to restore correct Windows Menu " + $_.FullyQualifiedErrorId) + } +} + +installClassicShell +setClassicShellRegistryKeys +stopClassicShell + +refreshenv + +exit 0 diff --git a/provisioning/data/logo/win-logo.png b/provisioning/data/logo/win-logo.png new file mode 100644 index 000000000..7ec69211d Binary files /dev/null and b/provisioning/data/logo/win-logo.png differ diff --git a/provisioning/data/menuLayouts/StartMenuLayout.xml b/provisioning/data/menuLayouts/StartMenuLayout.xml new file mode 100644 index 000000000..b31040724 --- /dev/null +++ b/provisioning/data/menuLayouts/StartMenuLayout.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/provisioning/data/scripts/ImmersiveShellRecover.ps1 b/provisioning/data/scripts/ImmersiveShellRecover.ps1 new file mode 100644 index 000000000..8a93fe3e4 --- /dev/null +++ b/provisioning/data/scripts/ImmersiveShellRecover.ps1 @@ -0,0 +1,13 @@ +<# + . +#> + +$VerbosePreference = "continue" + +$GPII_Data_Path = Join-Path $env:LOCALAPPDATA "GPII" + +Write-Verbose("Starting $PSCommandPath and set GPII_Data_Path: $GPII_Data_Path") + +$TabletModeScript = Join-Path $GPII_Data_Path "scripts\TabletView.ahk" +Write-Verbose("Launching tabletModeScript : $TabletModeScript") +Start-Process -FilePath $TabletModeScript 1 diff --git a/provisioning/data/scripts/ImmersiveShellSetter.ps1 b/provisioning/data/scripts/ImmersiveShellSetter.ps1 new file mode 100644 index 000000000..7b2c4bbd2 --- /dev/null +++ b/provisioning/data/scripts/ImmersiveShellSetter.ps1 @@ -0,0 +1,13 @@ +<# + . +#> + +$VerbosePreference = "continue" + +$GPII_Data_Path = Join-Path $env:LOCALAPPDATA "GPII" + +Write-Verbose("Starting $PSCommandPath and set GPII_Data_Path: $GPII_Data_Path") + +$TabletModeScript = Join-Path $GPII_Data_Path "scripts\TabletView.ahk" +Write-Verbose("Launching tabletModeScript : $TabletModeScript") +Start-Process -FilePath $TabletModeScript 0 diff --git a/provisioning/data/scripts/StartMenuRecover.ps1 b/provisioning/data/scripts/StartMenuRecover.ps1 new file mode 100644 index 000000000..856867fc0 --- /dev/null +++ b/provisioning/data/scripts/StartMenuRecover.ps1 @@ -0,0 +1,49 @@ +<# + . +#> + +# Get the ID and security principal of the current user account +$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() +$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) + +# Get the security principal for the Administrator role +$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator + +# Check to see if we are currently running "as Administrator" +if ($myWindowsPrincipal.IsInRole($adminRole)) { + # We are running "as Administrator" - so change the title and background color to indicate this + $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)" + $Host.UI.RawUI.BackgroundColor = "DarkBlue" + clear-host +} else { + # We are not running "as Administrator" - so relaunch as administrator + + # Create a new process object that starts PowerShell + $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"; + # Specify the current script path and name as a parameter + $newProcess.Arguments = $myInvocation.MyCommand.Definition; + # Indicate that the process should be elevated + $newProcess.Verb = "runas"; + # Avoid the display of the window. + $newProcess.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden; + # Start the new process + [System.Diagnostics.Process]::Start($newProcess); + + # Exit from the current, unelevated, process + exit +} + +$VerbosePreference = "continue" + +$GPII_Data_Path = Join-Path $env:LOCALAPPDATA "GPII" + +Write-Verbose("Starting $PSCommandPath and set GPII_Data_Path: $GPII_Data_Path") + +$CurrentLayout = Join-Path $GPII_Data_Path "menuLayouts\current_layout.xml" + +reg add "HKCU\Software\Policies\Microsoft\Windows\Explorer" /f /v "StartLayoutFile" /t REG_SZ /d "$CurrentLayout" +reg add "HKCU\Software\Policies\Microsoft\Windows\Explorer" /f /v "LockedStartLayout" /t REG_DWORD /d "1" + +taskkill /f /im "ShellExperienceHost.exe" + +exit diff --git a/provisioning/data/scripts/StartMenuSetter.ps1 b/provisioning/data/scripts/StartMenuSetter.ps1 new file mode 100644 index 000000000..e8be86168 --- /dev/null +++ b/provisioning/data/scripts/StartMenuSetter.ps1 @@ -0,0 +1,75 @@ +<# + . +#> + +############################################################################# +# If Powershell is running the 32-bit version on a 64-bit machine, we +# need to force powershell to run in 64-bit mode . +############################################################################# + +if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") { + write-warning "Not running in 64bits, relaunching script in 64 bit mode" + if ($myInvocation.Line) { + &"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile $myInvocation.Line + }else{ + &"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -file "$($myInvocation.InvocationName)" $args + } +exit $lastexitcode +} + +# Get the ID and security principal of the current user account +$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() +$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) + +# Get the security principal for the Administrator role +$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator + +# Check to see if we are currently running "as Administrator" +if ($myWindowsPrincipal.IsInRole($adminRole)) { + # We are running "as Administrator" - so change the title and background color to indicate this + $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)" + $Host.UI.RawUI.BackgroundColor = "DarkBlue" + clear-host +} else { + # We are not running "as Administrator" - so relaunch as administrator + + # Create a new process object that starts PowerShell + $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"; + # Specify the current script path and name as a parameter + $newProcess.Arguments = $myInvocation.MyCommand.Definition; + # Indicate that the process should be elevated + $newProcess.Verb = "runas"; + # Avoid the display of the window. + $newProcess.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden; + # Start the new process + [System.Diagnostics.Process]::Start($newProcess); + + # Exit from the current, unelevated, process + exit +} + +$VerbosePreference = "continue" +$ErrorActionPreference = "Stop" + +$GPII_Data_Path = Join-Path $env:LOCALAPPDATA "GPII" + +Write-Verbose("Starting $PSCommandPath and set GPII_Data_Path : $GPII_Data_Path ") + +Write-Verbose("Exporting current layout.") +$CurrentLayout = Join-Path $GPII_Data_Path "menuLayouts\current_layout.xml" +$CurError = Join-Path $GPII_Data_Path "menuLayouts\Error.txt" + +Try { + Export-StartLayout -Path $CurrentLayout -ErrorAction Stop +} Catch { + $ErrorMessage = $_.Exception.Message + New-Item -Path $CurError -type file -force -value $ErrorMessage + Break +} + +$NewLayout = Join-Path $GPII_Data_Path "menuLayouts\StartMenuLayout.xml" +Write-Verbose("Writing layout from $NewLayout") +reg add "HKCU\Software\Policies\Microsoft\Windows\Explorer" /f /v "StartLayoutFile" /t REG_SZ /d "$NewLayout" +reg add "HKCU\Software\Policies\Microsoft\Windows\Explorer" /f /v "LockedStartLayout" /t REG_DWORD /d "1" + +taskkill /f /im "ShellExperienceHost.exe" diff --git a/provisioning/data/scripts/TabletView.ahk b/provisioning/data/scripts/TabletView.ahk new file mode 100644 index 000000000..d1d010d93 --- /dev/null +++ b/provisioning/data/scripts/TabletView.ahk @@ -0,0 +1,23 @@ +SetBatchLines -1 +ListLines Off + +SELECTED_MODE = %1% +TABLETMODESTATE_DESKTOPMODE := 0x0 +TABLETMODESTATE_TABLETMODE := 0x1 + +TabletModeController_GetMode(TabletModeController, ByRef mode) { + return DllCall(NumGet(NumGet(TabletModeController+0),3*A_PtrSize), "Ptr", TabletModeController, "UInt*", mode) +} + +TabletModeController_SetMode(TabletModeController, _TABLETMODESTATE, _TMCTRIGGER := 4) { + return DllCall(NumGet(NumGet(TabletModeController+0),4*A_PtrSize), "Ptr", TabletModeController, "UInt", _TABLETMODESTATE, "UInt", _TMCTRIGGER) +} + +ImmersiveShell := ComObjCreate("{C2F03A33-21F5-47FA-B4BB-156362A2F239}", "{00000000-0000-0000-C000-000000000046}") +TabletModeController := ComObjQuery(ImmersiveShell, "{4fda780a-acd2-41f7-b4f2-ebe674c9bf2a}", "{4fda780a-acd2-41f7-b4f2-ebe674c9bf2a}") + +if (TabletModeController_GetMode(TabletModeController, mode) == 0) + TabletModeController_SetMode(TabletModeController, SELECTED_MODE == TABLETMODESTATE_DESKTOPMODE ? TABLETMODESTATE_TABLETMODE : TABLETMODESTATE_DESKTOPMODE) + +ObjRelease(TabletModeController), TabletModeController := 0 +ObjRelease(ImmersiveShell), ImmersiveShell := 0 ; Can be freed after TabletModeController is created, instead diff --git a/provisioning/data/wallpapers/img0.jpg b/provisioning/data/wallpapers/img0.jpg new file mode 100644 index 000000000..a41881468 Binary files /dev/null and b/provisioning/data/wallpapers/img0.jpg differ diff --git a/provisioning/data/wallpapers/img1.jpg b/provisioning/data/wallpapers/img1.jpg new file mode 100644 index 000000000..a0cf53346 Binary files /dev/null and b/provisioning/data/wallpapers/img1.jpg differ diff --git a/provisioning/data/wallpapers/img2.jpg b/provisioning/data/wallpapers/img2.jpg new file mode 100644 index 000000000..4564c17ae Binary files /dev/null and b/provisioning/data/wallpapers/img2.jpg differ diff --git a/provisioning/envChanges/CreateDataDir.ps1 b/provisioning/envChanges/CreateDataDir.ps1 new file mode 100644 index 000000000..f87a56b12 --- /dev/null +++ b/provisioning/envChanges/CreateDataDir.ps1 @@ -0,0 +1,19 @@ +<# + Script that createss the folder for holding the necessary data for GPII. +#> + +$GPII_Data_Path = "$env:LOCALAPPDATA\GPII" + +if (!(Test-Path $GPII_Data_Path)) { + New-Item -Path $GPII_Data_Path -ItemType 'directory' | Out-Null +} + +$mainDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$dataDir = Join-Path $mainDir "..\data\*" + +Write-Verbose("Deleting: '$GPII_Data_Path'") +Remove-Item "$GPII_Data_Path\*" -Force -Recurse +Write-Verbose("Copying: '$dataDir' to '$GPII_Data_Path'") +Copy-Item -Path $dataDir -Force -Recurse -Destination $GPII_Data_Path -ErrorAction Stop + +exit 0