UNPKG

gpii-windows

Version:

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

1,003 lines (892 loc) 36.2 kB
/* * Captures metrics specific to the Windows operating system - for example, non-identifying keyboard metrics, * activation and deactivation of top-level windows, etc. * * Copyright 2017 Raising the Floor - International * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * * The R&D leading to these results received funding from the * Department of Education - Grant H421A150005 (GPII-APCP). However, * these results do not necessarily represent the policy of the * Department of Education, and you should not assume endorsement by the * Federal Government. * * You may obtain a copy of the License at * https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; var ffi = require("ffi-napi"); var fluid = require("gpii-universal"), path = require("path"); var gpii = fluid.registerNamespace("gpii"), windows = fluid.registerNamespace("gpii.windows"); fluid.registerNamespace("gpii.windows.metrics"); require("../../WindowsUtilities/WindowsUtilities.js"); require("../../windowMessages"); require("../../displaySettingsHandler"); fluid.defaults("gpii.windowsMetrics", { gradeNames: ["fluid.modelComponent", "fluid.contextAware", "gpii.metrics"], contextAwareness: { platform: { checks: { test: { contextValue: "{gpii.contexts.test}", gradeNames: "gpii.windowsMetrics.test" }, windows: { contextValue: "{gpii.contexts.windows}", gradeNames: "gpii.windowsMetrics.windows" } } } }, listeners: { "onDestroy.stopMetrics": "{that}.events.onStopMetrics", "{gpii.eventLog}.events.onCreate": [{ func: "{that}.logVersions", priority: "last" }, { func: "{that}.logSystemInfo", priority: "last" }], "onStartMetrics.application": "{that}.startApplicationMetrics", "onStopMetrics.application": "{that}.stopApplicationMetrics", "onStartMetrics.input": "{that}.startInputMetrics", "onStopMetrics.input": "{that}.stopInputMetrics", "{gpii.windows.messages}.events.onMessage": { funcName: "gpii.windows.metrics.windowMessage", // that, hwnd, msg, wParam, lParam args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3" ] } }, invokers: { logMetric: { func: "{eventLog}.logEvent", args: ["metrics", "{arguments}.0", "{arguments}.1"] }, logVersions: { funcName: "gpii.windows.metrics.logVersions", args: ["{that}"] }, logSystemInfo: { funcName: "gpii.windows.metrics.logSystemInfo", args: ["{that}"] }, startApplicationMetrics: { funcName: "gpii.windows.metrics.startApplicationMetrics", args: ["{that}"] }, stopApplicationMetrics: { funcName: "gpii.windows.metrics.stopApplicationMetrics", args: ["{that}"] }, startInputMetrics: { funcName: "gpii.windows.metrics.startInputMetrics", args: ["{that}"] }, stopInputMetrics: { funcName: "gpii.windows.metrics.stopInputMetrics", args: ["{that}"] }, windowActivated: { funcName: "gpii.windows.metrics.windowActivated", args: ["{that}", "{arguments}.0"] // window handle (hwnd) }, windowCreated: { funcName: "gpii.windows.metrics.windowCreated", args: ["{that}", "{arguments}.0", "{arguments}.1"] // window handle (hwnd) }, windowDestroyed: { funcName: "gpii.windows.metrics.windowDestroyed", args: ["{that}", "{arguments}.0", "{arguments}.1"] // window handle (hwnd) }, startMessages: "{gpii.windows.messages}.start({that})", stopMessages: "{gpii.windows.messages}.stop({that})", getMessageWindow: "{gpii.windows.messages}.getWindowHandle()" }, members: { config: { application: { }, input: { // Minimum typing session time, in milliseconds. minSession: 30000, // The time a session will last with no activity, in milliseconds. sessionTimeout: 60000, // Minimum number of keys in a typing session time. minSessionKeys: 30000, // Milliseconds of no input to assume inactive inactiveTime: 300000 } }, state: { application: { // List of windows that are known to exist. knownWindows: {}, // The running applications runningApplications: {} }, input: { lastKeyTime: 0, // Timestamp of typing session start. sessionStart: null, // Number of keys in the typing session. keyCount: 0, // Number of special keys specialCount: 0, // Mouse position lastPos: null, distance: 0, lastInputTime: 0 } }, keyboardHookHandle: null, mouseHookHandle: null } }); fluid.defaults("gpii.installID.windows", { invokers: { getMachineID: "gpii.windows.getMachineID" } }); /** * Gets the machine ID - something that uniquely identifies this machine. * * This relies on the MachineGUID, which is generated when Windows is installed or when a cloned image is deployed * in the recommended way using sysprep. * * @return {String} The machine ID. */ windows.getMachineID = function () { var machineID = windows.readRegistryKey( "HKEY_LOCAL_MACHINE", "64:SOFTWARE\\Microsoft\\Cryptography", "MachineGuid", "REG_SZ").value; return machineID; }; /** * Logs the version of this, the gpii-app, gpii-windows, and gpii-universal modules. * * The logging of this module can be used to identify changes in the data structures, independent of the other releases. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.logVersions = function (that) { var data = {}; var modules = [ "windowsMetrics", "gpii-app", "gpii-windows", "gpii-universal" ]; fluid.each(modules, function (moduleName) { var version; if (fluid.module.modules[moduleName]) { var packageData = fluid.require("%" + moduleName + "/package.json"); version = packageData.version; } else { version = "none"; } data[moduleName] = version; }); that.logMetric("version", data); }; /** * Logs information about the system, consisting of: * * - CPU & memory. * - Windows version. * - System name and manufacturer. * * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.logSystemInfo = function (that) { var os = require("os"); var oneGB = 0x40000000; var cpus = os.cpus(); var resolution = windows.display.getScreenResolution(); var desktop = windows.display.getDesktopSize(); var scale = (resolution.width / desktop.width).toPrecision(3); var data = { cpu: cpus[0].model, cores: cpus.length, memory: (os.totalmem / oneGB).toPrecision(2), resolution: resolution.width + "x" + resolution.height, scale: scale, osRelease: os.release(), osEdition: windows.readRegistryKey("HKEY_LOCAL_MACHINE", "64:SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ProductName", "REG_SZ").value, osBits: windows.isWow64() || os.arch() === "x64" ? "64" : "32", systemMfr: windows.readRegistryKey("HKEY_LOCAL_MACHINE", "64:SYSTEM\\CurrentControlSet\\Control\\SystemInformation", "SystemManufacturer", "REG_SZ").value, systemName: windows.readRegistryKey("HKEY_LOCAL_MACHINE", "64:SYSTEM\\CurrentControlSet\\Control\\SystemInformation", "SystemProductName", "REG_SZ").value }; that.logMetric("system-info", data); }; /** * Check if the currently active pid+exe has been seen before. If not, log it as an application launch. * Called when a window has been activated. * * @param {Component} that The gpii.windowsMetrics instance. * @param {WindowInfo} windowInfo The new window. */ windows.metrics.checkNewApplication = function (that, windowInfo) { var runningApplications = that.state.application.runningApplications; // pid might have been re-used. var oldApp = runningApplications[windowInfo.pid]; var isNew = !oldApp || oldApp.exe !== windowInfo.exe; if (isNew) { var data = { exe: windows.metrics.genericisePath(windowInfo.exe), pid: windowInfo.pid, windowClass: windowInfo.className }; that.logMetric("app-launch", data); runningApplications[windowInfo.pid] = { exe: windowInfo.exe }; } }; /** * Begin monitoring the application launches and active windows. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.startApplicationMetrics = function (that) { that.startMessages(); // Tell Windows to send WM_SHELLHOOKMESSAGE. gpii.windows.user32.RegisterShellHookWindow(that.getMessageWindow()); }; /** * Stops collecting the application metrics. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.stopApplicationMetrics = function (that) { that.stopMessages(); }; /** * Logs the application active metric - how long an application has been active for. * Called when a new window is being activated, while currentProcess refers to the application losing focus. * * @param {Component} that The gpii.windowsMetrics instance. * @param {WindowInfo} windowInfo The activated window. */ windows.metrics.logAppActivate = function (that, windowInfo) { var data = { exe: windows.metrics.genericisePath(windowInfo.exe), window: windowInfo.pid.toString(36) + "-" + windowInfo.hwnd.toString(36), windowClass: windowInfo.className }; that.logMetric("app-active", data); }; /** * Called when an event has been received by the message window. * * @param {Component} that The gpii.windowsMetrics component. * @param {Number} hwnd The window handle of the message window. * @param {Number|String} msg The message identifier. * @param {Number} wParam Message specific data. * @param {Object} lParam Additional message specific data (passed, but not used). */ windows.metrics.windowMessage = function (that, hwnd, msg, wParam, lParam) { var lParamNumber = (lParam && lParam.address) ? lParam.address() : lParam || 0; switch (msg) { case windows.API_constants.WM_SHELLHOOK: // Run the code in the next tick so this function can return soon, as it's a window procedure. process.nextTick(windows.metrics.shellMessage, that, wParam, lParamNumber); break; default: if (windows.metrics.settingsMessages.indexOf(msg) > -1) { process.nextTick(windows.metrics.configMessage, that, hwnd, msg, wParam, lParamNumber); } break; } }; /** * Handles the WM_SHELLHOOKMESSAGE system notification. * https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-registershellhookwindow * * @param {Component} that The gpii.windowsMetrics component. * @param {Number} message The type of shell hook message. * @param {Number} hwnd The window handle the message is about. */ windows.metrics.shellMessage = function (that, message, hwnd) { switch (message) { case windows.API_constants.HSHELL_WINDOWACTIVATED: case windows.API_constants.HSHELL_RUDEAPPACTIVATED: that.windowActivated(hwnd); break; case windows.API_constants.HSHELL_WINDOWCREATED: if (hwnd) { that.windowCreated(hwnd); } break; case windows.API_constants.HSHELL_WINDOWDESTROYED: that.windowDestroyed(hwnd); break; } }; // The interesting messages for metrics that are received when a setting has changed. windows.metrics.settingsMessages = [ windows.API_constants.WM_SYSCOLORCHANGE, windows.API_constants.WM_INPUTLANGCHANGE, windows.API_constants.WM_DISPLAYCHANGE, windows.API_constants.WM_THEMECHANGED, windows.API_constants.WM_SETTINGCHANGE ]; /** * Log a configuration related windows message. Called when a message in windows.metrics.settingsMessages has been * received. * * @param {Component} that The gpii.windowsMetrics component. * @param {Number} hwnd The window handle of the message window. * @param {Number} msg The message identifier. * @param {Number} wParam Message specific data. * @param {Number} lParam Additional message specific data. */ windows.metrics.configMessage = function (that, hwnd, msg, wParam, lParam) { var eventData = { wp: wParam, lp: lParam, msg: msg }; // Get the constant name (eg, "WM_DISPLAYCHANGE") eventData.msg = fluid.find(windows.API_constants, function (value, key) { if (value === msg && key.startsWith("WM_")) { return key; } }); var eventName; switch (msg) { case windows.API_constants.WM_SETTINGCHANGE: // "when the SystemParametersInfo function changes a system-wide setting or when policy settings have changed" // https://docs.microsoft.com/en-us/windows/desktop/winmsg/wm-settingchange var spiAction = gpii.windows.spi.actionsLookup[wParam]; if (spiAction) { eventName = "spi"; eventData.action = spiAction; } break; case windows.API_constants.WM_DISPLAYCHANGE: // "when the display resolution has changed" // https://docs.microsoft.com/en-gb/windows/desktop/gdi/wm-displaychange eventName = "resolution"; eventData.width = windows.loWord(lParam); eventData.height = windows.hiWord(lParam); // Assume it's always 32bpp, unless specified. if (wParam !== 32) { eventData.bpp = wParam; } break; default: break; } eventName = eventName ? "config." + eventName : "config"; that.logMetric(eventName, eventData); }; /** * Called when a window has been created, or an unknown window has been activated. * * Adds the window to the list of known windows along with the process, so when the window is destroyed it can be * determined which process it belonged to. * * @param {Component} that The gpii.windowsMetrics component. * @param {Number} hwnd The window handle. */ windows.metrics.windowCreated = function (that, hwnd) { var state = that.state.application; var windowInfo = state.knownWindows[hwnd]; if (!windowInfo) { windowInfo = windows.metrics.getWindowInfo(hwnd); state.knownWindows[hwnd] = windowInfo; } windows.metrics.checkNewApplication(that, windowInfo); }; /** * Called when a window has been destroyed. * * Logs the app-close event, if the process terminates soon. * * @param {Component} that The gpii.windowsMetrics component. * @param {Number} hwnd The window handle (no longer valid). */ windows.metrics.windowDestroyed = function (that, hwnd) { var state = that.state.application; // Use the stored info, because the window has been destroyed and the handle is invalid. var windowInfo = state.knownWindows[hwnd]; if (windowInfo) { var runningApp = state.runningApplications[windowInfo.pid]; if (runningApp && !runningApp.closing) { // Wait a bit for the process to close. windows.waitForProcessTermination(windowInfo.pid, {pollDelay: 1000, timeout: 20000}).then(function () { var data = { exe: windows.metrics.genericisePath(windowInfo.exe), pid: windowInfo.pid, windowClass: windowInfo.className }; that.logMetric("app-close", data); delete state.runningApplications[windowInfo.pid]; }, function () { // timeout - the process isn't closing after all. runningApp.closing = false; }); // Don't log the close again, for multi-window processes. runningApp.closing = true; } delete state.knownWindows[hwnd]; } }; /** * Called when a window has been activated. * @param {Component} that The gpii.windowsMetrics component. * @param {Number} hwnd The window handle. */ windows.metrics.windowActivated = function (that, hwnd) { var state = that.state.application; if (!hwnd) { hwnd = windows.user32.GetForegroundWindow(); } if (hwnd && hwnd !== state.activeWindow) { var windowInfo = state.knownWindows[hwnd]; if (!windowInfo) { // The window isn't known - it may have been created before this process. that.windowCreated(hwnd); windowInfo = state.knownWindows[hwnd]; } if (windowInfo) { that.state.application.currentProcess = { pid: windowInfo.pid, exe: windows.metrics.genericisePath(windowInfo.exe) }; windows.metrics.logAppActivate(that, windowInfo); } } state.activeWindow = hwnd; }; /** * Information about a window. * @typedef {Object} WindowInfo * @property {Number} hwnd The window handle. * @property {String} className The name of the window class. * @property {Number} pid The process ID. * @property {String} exe The executable name (lower-cased, without the directory) */ /** * Gets some pieces of information about a window. * * @param {Number} hwnd The window handle. * @return {WindowInfo} Information about the window. */ windows.metrics.getWindowInfo = function (hwnd) { var windowInfo = { hwnd: hwnd, pid: windows.getWindowProcessId(hwnd), exe: null, className: null }; if (windowInfo.pid) { windowInfo.exe = windows.getProcessPath(windowInfo.pid); } else { windowInfo.pid = 0; } if (!windowInfo.exe) { windowInfo.exe = "unknown-" + windowInfo.pid.toString(16); } var classBuffer = Buffer.alloc(0xff); var len = windows.user32.GetClassNameW(hwnd, classBuffer, classBuffer.length); if (len > 0) { windowInfo.className = windows.stringFromWideChar(classBuffer); } // For UWP apps, the main window doesn't belong to the real process; the application window is a child. var appClass = "Windows.UI.Core.CoreWindow"; if (windowInfo.className !== appClass && windowInfo.exe && windowInfo.exe.toLowerCase().endsWith("applicationframehost.exe")) { classBuffer = Buffer.alloc(0xff); var child = windows.enumerateWindows(hwnd, function (hwndChild) { if (windows.user32.GetClassNameW(hwndChild, classBuffer, classBuffer.length)) { var cls = windows.stringFromWideChar(classBuffer); return cls === appClass ? hwndChild : undefined; } }); if (child) { // Use the info from the child window. windowInfo = windows.metrics.getWindowInfo(child); windowInfo.hwnd = hwnd; } } return windowInfo; }; /** * The environment variables used by genericisePath() to translate real paths into paths with environment variables. * These are compared with the start of a path, in the given order. * @type {Array<String>} */ windows.metrics.pathEnvironmentNames = [ "SystemRoot", // C:\Windows "CommonProgramFiles", // C:\Program Files\Common Files "CommonProgramFiles(x86)", // C:\Program Files (x86)\Common Files "ProgramFiles(x86)", // C:\Program Files (x86) "ProgramFiles", // C:\Program Files "APPDATA", // C:\Users\vagrant\AppData\Roaming "TEMP", // C:\Users\vagrant\AppData\Local\Temp "LOCALAPPDATA", // C:\Users\vagrant\AppData\Local "HOME", // C:\Users\vagrant "USERPROFILE", // C:\Users\vagrant "PUBLIC", // C:\Users\Public "ProgramData", // C:\ProgramData "ALLUSERSPROFILE" // C:\ProgramData ]; /** * Takes a real path, and attempts to produce a generic looking path which is prefixed with a well-known environment * variable instead of the real path. * * For example, "C:\Users\yourname\some-file" becomes "%HOME%\some-file" * * This is mostly to stop the user name being leaked (eg, `c:\Users\<username>\`), but also makes certain paths look the * same when from other systems with different names (such as `C:\Archivos de programa` and `C:\Program Files`) * * @param {String} rawPath The path. * @param {Object} env [optional] The environment map (default: process.env) * @return {String} The path, either as-is or with a matching environment variable name replacing its value. */ windows.metrics.genericisePath = function (rawPath, env) { env = env || process.env; if (!rawPath) { return ""; } var pathMatch = path.normalize(rawPath).toLowerCase(); var envFound = fluid.find(windows.metrics.pathEnvironmentNames, function (envName) { if (env[envName]) { var envValue = path.normalize(env[envName]).replace(/\\+$/, ""); if (pathMatch.startsWith(envValue.toLowerCase())) { return { name: envName, value: envValue }; } } }); var pathTogo; if (envFound) { var ending = path.normalize(rawPath).substr(envFound.value.length); pathTogo = path.join("%" + envFound.name + "%", ending); } else { pathTogo = rawPath; } return pathTogo; }; /** * Starts the input metrics. * * This sets up low-level keyboard and mouse hooks (WH_KEYBOARD_LL, WH_MOUSE_LL) that makes the system invoke a * call-back whenever a key is pressed or released, or the mouse is moved/clicked. * Hooks overview: https://msdn.microsoft.com/library/ms644959 * * This comes with the following limitations: * - The process needs a window-message loop. Fortunately, Electron has one so this means it will only work if running * via gpii-app. * - It can't see the keys that are destined to a window owned by a process running as Administrator (and rightly so). * - The hook call-back has to return in a timely manner - not only because it would cause key presses to lag, but also * Windows will silently remove the hook if it times out. The time is unspecified (but it can be set in the registry). * - Anti-virus software may question this. * * The environment variable GPII_NO_INPUT_METRICS can be set to disable input metrics. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.startInputMetrics = function (that) { windows.metrics.stopInputMetrics(that); var disable = process.env.GPII_NO_INPUT_METRICS; if (disable) { fluid.log(fluid.logLevel.WARN, "Input metrics disabled with GPII_NO_INPUT_METRICS."); that.logMetrics("input-disabled"); } else if (process.versions.electron || that.options.forceInputMetrics) { var callHook = function (code, wparam, lparam) { // Handle the hook in the next tick, to allow the hook callback to return quickly. The lparam data needs to // be copied, otherwise the memory will be re-used. var dup = new lparam.type(); lparam.copy(dup.ref()); process.nextTick(windows.metrics.inputHook, that, code, wparam, dup); return windows.user32.CallNextHookEx(0, code, wparam, lparam); }; // The callbacks need to be referenced outside here otherwise the GC will pull the rug from beneath it. windows.metrics.keyboardHookCallback = ffi.Callback( windows.types.HANDLE, [windows.types.INT, windows.types.HANDLE, windows.KBDLLHookStructPointer], callHook); windows.metrics.mouseHookCallback = ffi.Callback( windows.types.HANDLE, [windows.types.INT, windows.types.HANDLE, windows.MSDLLHookStructPointer], callHook); var WH_KEYBOARD_LL = 13, WH_MOUSE_LL = 14; var hModule = windows.kernel32.GetModuleHandleW(0); // Start the keyboard hook that.keyboardHookHandle = windows.user32.SetWindowsHookExW(WH_KEYBOARD_LL, windows.metrics.keyboardHookCallback, hModule, 0); if (!that.keyboardHookHandle) { var errCode = windows.kernel32.GetLastError(); windows.metrics.stopInputMetrics(that); fluid.fail("SetWindowsHookExW did not work (keyboard). win32 error: " + errCode); } // Start the mouse hook that.mouseHookHandle = windows.user32.SetWindowsHookExW(WH_MOUSE_LL, windows.metrics.mouseHookCallback, hModule, 0); if (!that.mouseHookHandle) { var errCode2 = windows.kernel32.GetLastError(); windows.metrics.stopInputMetrics(that); fluid.fail("SetWindowsHookExW did not work (mouse). win32 error: " + errCode2); } windows.metrics.userInput(that); } else { // The keyboard hook's ability to work is a side-effect of running with electron. fluid.log(fluid.logLevel.WARN, "Input metrics not available without Electron."); } }; /** * Disables the key stroke metrics. * * Removes the low-level keyboard hook from the system, and sends the last timings to the log. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.stopInputMetrics = function (that) { var state = that.state.input; if (state) { if (state.inactivityTimer) { clearTimeout(state.inactivityTimer); state.inactivityTimer = null; } } // remove the hooks if (that.keyboardHookHandle) { windows.user32.UnhookWindowsHookEx(that.keyboardHookHandle); } if (that.mouseHookHandle) { windows.user32.UnhookWindowsHookEx(that.mouseHookHandle); } that.keyboardHookHandle = null; that.mouseHookHandle = null; windows.metrics.keyboardHookCallback = null; windows.metrics.mouseHookCallback = null; }; /** * A value=>name map of only non-printable keys that will be logged. */ windows.metrics.specialKeys = fluid.freezeRecursive((function () { // A white-list of key values that can be logged. It must not contain printable keys (like letters or numbers). // Also, there needs to be a matching value in windows.API_constants.virtualKeyCodes with the VK_ prefix. var keys = [ "BACK", "TAB", "RETURN", "ESCAPE", "PAGEUP", "PAGEDOWN", "END", "HOME", "LEFT", "UP", "RIGHT", "DOWN", "SELECT", "PRINT", "EXECUTE", "SNAPSHOT", "INSERT", "DELETE", "HELP", "LWIN", "RWIN", "SCROLL", "NUMLOCK", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", "CONTROL", "MENU", "SHIFT" ]; var special = {}; fluid.each(keys, function (keyName) { var value = windows.API_constants.virtualKeyCodes["VK_" + keyName]; if (value) { special[value] = keyName; } }); return special; })()); /** * Records the timing of a key press. This only logs the time between two keys being pressed, and not the actual * value of the key (unless it's a special key). Characters aren't being recorded. * * @param {Component} that The gpii.windowsMetrics instance. * @param {Number} timestamp Milliseconds since a fixed point in time. * @param {String} specialKey The value key, if it's a special key. * @param {String} keyValue Character value of the key. */ windows.metrics.recordKeyTiming = function (that, timestamp, specialKey, keyValue) { var state = that.state.input; var config = that.config.input; // The time since the last key press var keyTime = state.lastKeyTime ? timestamp - state.lastKeyTime : 0; if (keyTime > config.sessionTimeout) { // Only care about the time between keys in a typing session. keyTime = 0; } /* "A recordable typing session would be determined only once a threshold of thirty seconds of typing has been * reached and ending after a period of not typing for 60 seconds. (the recorded typing time for calculation would * include the 30 seconds for threshold and exclude the 60 seconds inactivity session end threshold)" */ if ((state.keyCount > 1) && !keyTime) { var duration = state.lastKeyTime - state.sessionStart; if (duration > config.minSession && state.keyCount >= config.minSessionKeys) { // Record the typing rate for the last typing session. var data = { duration: duration, count: state.keyCount, corrections: state.specialCount }; // Keys per minute. data.rate = Math.round(60000 / data.duration * data.count); that.logMetric("typing-session", data); } state.keyCount = 0; } if (!state.keyCount) { if (!specialKey) { // New typing session. state.keyCount = 1; state.specialCount = 0; state.sessionStart = timestamp; } } else if (specialKey) { state.specialCount++; } else { state.keyCount++; } state.lastKeyTime = timestamp; var record = { keyTime: keyTime }; var modifiers = windows.metrics.getModifierKeys(); var ctrl = false; if (modifiers.length > 0) { record.modifierKeys = modifiers; ctrl = modifiers.indexOf("CTRL") > -1; } if (specialKey) { // Double-check that only certain keys are being recorded (it would be a serious blunder). var keycode = parseInt(fluid.keyForValue(windows.metrics.specialKeys, specialKey)); if (!!keycode && typeof(specialKey) === "string" && specialKey.length > 1) { // Not logging the value of specialKey directly. record.key = windows.metrics.specialKeys[keycode]; } } if (ctrl) { record.key = keyValue; } that.logMetric("key-time", record); }; /** * Records a mouse event. Movement isn't logged, but the distance is accumulated. * * @param {Component} that The gpii.windowsMetrics instance. * @param {Number} button Mouse button: 0 movement only, 1 for primary (left), 2 for secondary, W for wheel. * @param {Object} pos Mouse cursor coordinates, and wheel distance (if applicable) {x, y, wheel}. */ windows.metrics.recordMouseEvent = function (that, button, pos) { var state = that.state.input; if (state.lastPos) { state.distance += Math.sqrt(Math.pow(state.lastPos.x - pos.x, 2) + Math.pow(state.lastPos.y - pos.y, 2)); } state.lastPos = pos; // log click or wheel events var data; if (pos.wheel) { data = {wheel: pos.wheel}; } else if (button) { data = { button: button, distance: Math.round(state.distance) }; // reset the distance accumulator state.distance = 0; } if (data) { // Add on the modifier keys. var modifiers = windows.metrics.getModifierKeys(); if (modifiers.length > 0) { data.modifierKeys = modifiers; } that.logMetric("mouse", data); } }; /** * Gets the modifier keys that are currently held down. * @return {Array<String>} May contain a combination of "CTRL", "ALT", and "SHIFT", identifying which keys are pressed. */ windows.metrics.getModifierKeys = function () { var modifiers = { "SHIFT": gpii.windows.API_constants.virtualKeyCodes.VK_SHIFT, "CTRL": gpii.windows.API_constants.virtualKeyCodes.VK_CONTROL, "ALT": gpii.windows.API_constants.virtualKeyCodes.VK_MENU }; var togo = []; fluid.each(modifiers, function (keycode, name) { var down = windows.user32.GetKeyState(keycode) & 0x8000; if (down) { togo.push(name); } }); return togo; }; /** * Called by inputHook when it receives some input, then waits for no further input to detect inactivity. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.userInput = function (that) { var state = that.state.input; if (state.inactive) { // First input from being inactive. var duration = process.hrtime(state.lastInputTime); that.logMetric("inactive-stop", { duration: duration[0] }); state.inactive = false; } if (state.inactivityTimer) { clearTimeout(state.inactivityTimer); state.inactivityTimer = null; } state.lastInputTime = process.hrtime(); state.inactivityTimer = setTimeout(windows.metrics.userInactive, that.config.input.inactiveTime, that); }; /** * Called when there's been some time since receiving input from the user. * * @param {Component} that The gpii.windowsMetrics instance. */ windows.metrics.userInactive = function (that) { that.state.input.inactive = true; that.logMetric("inactive-begin"); }; /** * The keyboard hook callback. Called when a key is pressed or released. * https://msdn.microsoft.com/library/ms644985 * * @param {Component} that The gpii.windowsMetrics instance. * @param {Number} code If less than zero, then don't process. * @param {Number} message The input message (eg, WM_KEYDOWN or WM_MOUSEMOVE). * @param {Number} eventData A KBDLLHOOKSTRUCT or MSDLLHOOKSTRUCT structure. */ windows.metrics.inputHook = function (that, code, message, eventData) { if (code >= 0) { windows.metrics.userInput(that); var wheelDistance; switch (message) { case windows.API_constants.WM_SYSKEYUP: case windows.API_constants.WM_KEYUP: // Key press var specialKey = windows.metrics.specialKeys[eventData.vkCode]; // Ignore injected keys. var ignoreFlags = windows.API_constants.LLKHF_INJECTED; if ((eventData.flags & ignoreFlags) === 0) { // If the key doesn't generate a character, then don't count it. var keyValue = windows.user32.MapVirtualKeyW(eventData.vkCode, windows.API_constants.MAPVK_VK_TO_CHAR); if (specialKey || keyValue) { windows.metrics.recordKeyTiming(that, eventData.time, specialKey, String.fromCharCode(keyValue)); } } break; case windows.API_constants.WM_MOUSEWHEEL: // "The high-order word indicates the distance the wheel is rotated, expressed in multiples or divisions of // WHEEL_DELTA, which is 120" var WHEEL_DELTA = 120; wheelDistance = windows.hiWord(eventData.mouseData); // unsigned to signed if (wheelDistance >= 0x8000) { wheelDistance -= 0x10000; } // number of notches wheelDistance /= WHEEL_DELTA; // fall-through case windows.API_constants.WM_MOUSEMOVE: case windows.API_constants.WM_LBUTTONUP: case windows.API_constants.WM_RBUTTONUP: // Log events that are not injected. if ((eventData.flags & windows.API_constants.LLMHF_INJECTED) === 0) { var button = wheelDistance ? 0 : (message >> 1) & 3; // 2nd + 3rd bits happen to map to the button windows.metrics.recordMouseEvent(that, button, { x: eventData.ptX, y: eventData.ptY, wheel: wheelDistance }); } break; } } };