From 9c5ea99906d479c07559c7c9a38b5444446870f2 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 5 May 2017 14:52:59 -0400 Subject: [PATCH 01/21] GPII-2404: Windows display settings handler should report available screen resolutions - Modified the display settings handler to get both the current and all available screen resolutions in one JSON object. - Refactored the handler into a fluid component. - Updated the unit tests in light of the above. --- .../src/displaySettingsHandler.js | 108 +++++++++++++++--- .../test/testDisplaySettingsHandler.js | 104 +++++++++++++++-- 2 files changed, 187 insertions(+), 25 deletions(-) diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index 150d9ca12..d4756c943 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -91,16 +91,65 @@ windows.display.createDevModeStruct = function () { return dm; }; +fluid.defaults("gpii.windowsDisplay", { + gradeNames: ["fluid.component"], + invokers: { + getScreenResolution: { + funcName: "gpii.windows.display.getScreenResolution", + args: [] + }, + getAvailableResolutions: { + funcName: "gpii.windows.display.getAvailableResolutions", + args: ["{that}"] + }, + getAllScreenResolutions: { + funcName: "gpii.windows.display.getAllScreenResolutions", + args: [] + }, + isScreenResolutionInvalid: { + funcName: "gpii.windows.display.isInvalid", + args: ["{that}", "{arguments}.0"] + // resolution to test + }, + setScreenResolution: { + funcName: "gpii.windows.display.setScreenResolution", + args: ["{that}", "{arguments}.0"] + // { width, height } + }, + getImpl: { + funcName: "gpii.windows.display.getImpl", + args: [] + }, + setImpl: { + funcName: "gpii.windows.display.setImpl", + args: ["{arguments}.0"] + // settingsRequest payload + }, + get: { + funcName: "gpii.windows.displaySettingsHandler.get", + args: ["{arguments}.0"] + // payload + }, + set: { + funcName: "gpii.windows.displaySettingsHandler.set", + args: ["{arguments}.0"] + // array of settings + } + } +}); + + /* * Returns if a screen resolution is invalid * - * @param (Object) + * @psrsm that {Component} An instance of gpii.displaySettingsHandler + * @param screenRes (Object) Screen resolution to test as {width, height} * @returns {boolean} true if invalid */ -windows.display.isInvalid = function (screenRes) { +gpii.windows.display.isInvalid = function (that, screenRes) { var isInvalid = true; if (typeof(screenRes.width) === "number" && typeof(screenRes.height) === "number" && screenRes.width > 0 && screenRes.height > 0) { - fluid.each(windows.display.getAvailableResolutions(), function (validScreenRes) { + fluid.each(that.getAvailableResolutions(), function (validScreenRes) { if (validScreenRes.dmPelsWidth === screenRes.width && validScreenRes.dmPelsHeight === screenRes.height) { isInvalid = false; }; @@ -114,7 +163,7 @@ windows.display.isInvalid = function (screenRes) { * * @return {Object) The width and height of the screen. */ -windows.display.getScreenResolution = function () { +gpii.windows.display.getScreenResolution = function () { var dm = windows.display.createDevModeStruct(); if (windows.display.user32.EnumDisplaySettingsW(ref.NULL, c.ENUM_CURRENT_SETTINGS, dm.ref()) !== c.FALSE) { // note for unknown reason on win 10 the returned dmSize is 188 not expected 220 @@ -130,7 +179,7 @@ windows.display.getScreenResolution = function () { * * @return {list} of resolutions */ -windows.display.getAvailableResolutions = function () { +gpii.windows.display.getAvailableResolutions = function () { var index = 0; var dm = windows.display.createDevModeStruct(); var availableResolutions = []; @@ -148,14 +197,31 @@ windows.display.getAvailableResolutions = function () { return availableResolutions; }; +/* + * Gets the current and all available screen resolutions + * + * @return {Object) The width and height as "current-resolution" and an array of + * widths/heights as "available-resolutions". + */ +gpii.windows.display.getAllScreenResolutions = function () { + var winDisplay = gpii.windowsDisplay(); + var currentResolution = winDisplay.getScreenResolution(); + var available = winDisplay.getAvailableResolutions(); + return { + "screen-resolution": currentResolution, + "available-resolutions": available + }; +}; + /* * Sets the current display's screen resolution if possible * + * @psrsm that {Component} An instance of gpii.displaySettingsHandler * @param {Object} The new screen resolution width and height * @return {boolean} true if successfu */ -windows.display.setScreenResolution = function (newRes) { - if (windows.display.isInvalid(newRes)) { +gpii.windows.display.setScreenResolution = function (that, newRes) { + if (that.isScreenResolutionInvalid(newRes)) { fluid.fail("Received an invalid screen resolution: ", newRes); } else { var dmCurrent = windows.display.createDevModeStruct(); @@ -183,8 +249,17 @@ windows.display.setScreenResolution = function (newRes) { } }; -windows.display.setImpl = function (payload) { - var oldRes = windows.display.getScreenResolution(); +/* + * Sets the current display's screen resolution based on payload. + * + * @param {Object} Payload that contains a "screen-resolution" property. + * @return {Object} containing the resolution before changing it ("oldValue"), + * and a copy of the new resolution (newRes), both with + * memmbers {width, height} + */ +gpii.windows.display.setImpl = function (payload) { + var windowsDisplay = gpii.windowsDisplay(); + var oldRes = windowsDisplay.getScreenResolution(); var newRes = oldRes; var targetRes = payload.settings["screen-resolution"]; @@ -192,7 +267,7 @@ windows.display.setImpl = function (payload) { fluid.fail("Incorrect payload for screen resolution: " + JSON.stringify(payload, null, 4)); } - else if (windows.display.setScreenResolution(targetRes)) { + else if (windowsDisplay.setScreenResolution(targetRes)) { newRes = targetRes; } @@ -203,11 +278,14 @@ windows.display.setImpl = function (payload) { return results; }; -windows.display.getImpl = function () { - var curRes = windows.display.getScreenResolution(); - var results = { "screen-resolution": curRes }; - - return results; +/* + * Returns the current display's screen resolution as. + * + * @return {Object} containing "screen-resolution" property that is the current + * resolution {width, height} + */ +gpii.windows.display.getImpl = function () { + return gpii.windows.display.getAllScreenResolutions(); }; windows.displaySettingsHandler.get = function (payload) { diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index 5037ac706..2572432f1 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -20,52 +20,136 @@ var gpii = fluid.registerNamespace("gpii"); require("../../WindowsUtilities/WindowsUtilities.js"); require("../src/displaySettingsHandler.js"); +var windowsDisplay = gpii.windowsDisplay(); + jqUnit.module("Windows Display Settings Handler Tests"); jqUnit.test("Testing GetScreenResolution", function () { jqUnit.expect(3); - var screenRes = gpii.windows.display.getScreenResolution(); + var screenRes = windowsDisplay.getScreenResolution(); jqUnit.assertDeepEq("getScreenResolution returns an object", "object", typeof(screenRes)); jqUnit.assertDeepEq("value for width is a number", "number", typeof(screenRes.width)); jqUnit.assertDeepEq("value for height is a number", "number", typeof(screenRes.height)); }); +jqUnit.test("Get available screen resolutions", function () { + var availableReesolutions = windowsDisplay.getAvailableResolutions(); + jqUnit.assertNotNull("Available resoiultions array", availableReesolutions); + jqUnit.assertNotEquals("Number of availableReesolutions", + 0, availableReesolutions.length); +}); + +jqUnit.test("Get all screen resolutions", function () { + var current = windowsDisplay.getScreenResolution(); + var available = windowsDisplay.getAvailableResolutions(); + var all = windowsDisplay.getAllScreenResolutions(); + jqUnit.assertDeepEq("All screen resolutions contains current resolution", + current, all["screen-resolution"]); + jqUnit.assertDeepEq("All screen resolutions contains available resolution", + available, all["available-resolutions"]); +}); + +jqUnit.test("Valid and invalid screen resolutions", function () { + var currentResolution = windowsDisplay.getScreenResolution(); + jqUnit.assertFalse( + "Valid screen resolution", + windowsDisplay.isScreenResolutionInvalid(currentResolution) + ); + var invalidResolution = { width: -999, height: -666 }; + jqUnit.assertTrue( + "Valid screen resolution", + windowsDisplay.isScreenResolutionInvalid(invalidResolution) + ); +}); + jqUnit.test("Testing setScreenResolution ", function () { - var oldRes = gpii.windows.display.getScreenResolution(); + var oldRes = windowsDisplay.getScreenResolution(); // We can change resolution // Not such a good unit test as depends on available modes and current screen resolution jqUnit.expect(2); var targetRes = { width: 800, height: 600 }; - jqUnit.assertTrue("We can call to setScreenResolution, it returns 'true'", gpii.windows.display.setScreenResolution(targetRes)); - var newRes = gpii.windows.display.getScreenResolution(); + jqUnit.assertTrue("We can call to setScreenResolution, it returns 'true'", + windowsDisplay.setScreenResolution(targetRes)); + var newRes = windowsDisplay.getScreenResolution(); jqUnit.assertDeepEq("New resolution is set", targetRes, newRes); // Restore old resolution jqUnit.expect(2); - jqUnit.assertTrue("Resetting to old resolution", gpii.windows.display.setScreenResolution(oldRes)); - var restoredRes = gpii.windows.display.getScreenResolution(); + jqUnit.assertTrue("Resetting to old resolution", windowsDisplay.setScreenResolution(oldRes)); + var restoredRes = windowsDisplay.getScreenResolution(); jqUnit.assertDeepEq("Old resolution appear to be restored", oldRes, restoredRes); // test can't change to invalid resolution var badRes = { width: -123, height: -123 }; jqUnit.expectFrameworkDiagnostic("setScreenResolution fails when receives an invalid resolution such as {w: -123, h: -123}", function () { - gpii.windows.display.setScreenResolution(badRes); + windowsDisplay.setScreenResolution(badRes); }, "invalid screen resolution"); // test that setScreenResolution fails when it receives a faulty screen resolution var faulty1 = 2; jqUnit.expectFrameworkDiagnostic("setScreenResolution fails when receives a non-object parameter", function () { - gpii.windows.display.setScreenResolution(faulty1); + windowsDisplay.setScreenResolution(faulty1); }, "invalid screen resolution"); var faulty2 = { w: 0, h: 1 }; jqUnit.expectFrameworkDiagnostic("setScreenResolution fails when receives a wrong object", function () { - gpii.windows.display.setScreenResolution(faulty2); + windowsDisplay.setScreenResolution(faulty2); }, ["invalid screen resolution"]); var faulty3 = { width: 0, height: "" }; jqUnit.expectFrameworkDiagnostic("setScreenResolution fails when receives one string as value for height", function () { - gpii.windows.display.setScreenResolution(faulty3); + windowsDisplay.setScreenResolution(faulty3); }, "invalid screen resolution"); }); + +jqUnit.test("WIndows display settings handlerr: getImpl()", function () { + var actualSettings = windowsDisplay.getImpl(); + var expectedSettings = windowsDisplay.getAllScreenResolutions(); + jqUnit.assertDeepEq("Return value of getImpl() with no input", + expectedSettings, actualSettings); +}); + +jqUnit.test("Windows display settings handler: setImpl()", function () { + var allRez = windowsDisplay.getAllScreenResolutions(); + var newRez = fluid.find(allRez["available-resolutions"], function (aRez) { + if (aRez.dmPelsWidth !== allRez["screen-resolution"].width || + aRez.dmPelsHeight !== allRez["screen-resolution"].height) { + return aRez; + } + }, null); + if (newRez !== null) { + var rezToSet = {}; + rezToSet.width = newRez.dmPelsWidth; + rezToSet.height = newRez.dmPelsHeight; + var screenResolution = {}; + fluid.set(screenResolution, "screen-resolution", rezToSet); + var payload = {}; + payload.settings = screenResolution; + var oldRez = windowsDisplay.setImpl(payload); + jqUnit.assertDeepEq( + "New resolution via setImpl()", + windowsDisplay.getScreenResolution(), + rezToSet + ); + jqUnit.assertDeepEq( + "Old resolution from setImpl()", + allRez["screen-resolution"], + oldRez["screen-resolution"].oldValue + ); + // Revert to the previous resolution. + payload.settings["screen-resolution"] = allRez["screen-resolution"]; + oldRez = windowsDisplay.setImpl(payload); + jqUnit.assertDeepEq( + "Revert to old resolution using setImpl()", + windowsDisplay.getScreenResolution(), + allRez["screen-resolution"] + ); + jqUnit.assertDeepEq( + "Old resolution returned during revert via setImpl()", + rezToSet, + oldRez["screen-resolution"].oldValue + ); + } +}); + From 99e0c9a5b9cdc34f59b5084b32d5887bc7653740 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Thu, 18 May 2017 16:55:32 -0400 Subject: [PATCH 02/21] GPII-2404: Windows display settings handler should report available screen resolutions Reverted getScreenResolution() to return an object with just width and height, but modified getImpl() to add a "screen-resolution" property. --- .../displaySettingsHandler/src/displaySettingsHandler.js | 4 +++- .../test/testDisplaySettingsHandler.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index d4756c943..d31d24993 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -285,7 +285,9 @@ gpii.windows.display.setImpl = function (payload) { * resolution {width, height} */ gpii.windows.display.getImpl = function () { - return gpii.windows.display.getAllScreenResolutions(); + var screenResolution = {}; + screenResolution["screen-resolution"] = gpii.windows.display.getScreenResolution(); + return screenResolution; }; windows.displaySettingsHandler.get = function (payload) { diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index 2572432f1..41137ecc2 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -103,11 +103,12 @@ jqUnit.test("Testing setScreenResolution ", function () { }, "invalid screen resolution"); }); -jqUnit.test("WIndows display settings handlerr: getImpl()", function () { +jqUnit.test("Windows display settings handlerr: getImpl()", function () { var actualSettings = windowsDisplay.getImpl(); var expectedSettings = windowsDisplay.getAllScreenResolutions(); jqUnit.assertDeepEq("Return value of getImpl() with no input", - expectedSettings, actualSettings); + expectedSettings["screen-resolution"], + actualSettings["screen-resolution"]); }); jqUnit.test("Windows display settings handler: setImpl()", function () { From 476f7d746cd6e42827669c9cda02aefbaa685b14 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 19 May 2017 09:57:06 -0400 Subject: [PATCH 03/21] GPII-2404: Windows display settings handler reports available screen resolutions Better way to set current screen resolution return value. Also, cleaned up some comments. --- .../displaySettingsHandler/src/displaySettingsHandler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index d31d24993..c6d8eb17e 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -109,7 +109,7 @@ fluid.defaults("gpii.windowsDisplay", { isScreenResolutionInvalid: { funcName: "gpii.windows.display.isInvalid", args: ["{that}", "{arguments}.0"] - // resolution to test + // { width, height } }, setScreenResolution: { funcName: "gpii.windows.display.setScreenResolution", @@ -177,7 +177,7 @@ gpii.windows.display.getScreenResolution = function () { /* * Gets available resolutions * - * @return {list} of resolutions + * @return {array} of resolutions */ gpii.windows.display.getAvailableResolutions = function () { var index = 0; @@ -279,14 +279,14 @@ gpii.windows.display.setImpl = function (payload) { }; /* - * Returns the current display's screen resolution as. + * Returns the display's current screen resolution. * * @return {Object} containing "screen-resolution" property that is the current * resolution {width, height} */ gpii.windows.display.getImpl = function () { var screenResolution = {}; - screenResolution["screen-resolution"] = gpii.windows.display.getScreenResolution(); + fluid.set(screenResolution, "screen-resolution", gpii.windows.display.getScreenResolution()); return screenResolution; }; From 0e78ce7c8c3e5c17ec1f31932d67b72951e3f21f Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Wed, 5 Jul 2017 16:46:04 -0400 Subject: [PATCH 04/21] GPII-2404: Windows display settings handler reports available screen resolutions Removed unused functions. --- .../src/displaySettingsHandler.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index c6d8eb17e..fb45f79a7 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -124,16 +124,6 @@ fluid.defaults("gpii.windowsDisplay", { funcName: "gpii.windows.display.setImpl", args: ["{arguments}.0"] // settingsRequest payload - }, - get: { - funcName: "gpii.windows.displaySettingsHandler.get", - args: ["{arguments}.0"] - // payload - }, - set: { - funcName: "gpii.windows.displaySettingsHandler.set", - args: ["{arguments}.0"] - // array of settings } } }); From b06be70c49d1b9d63fe08542aee7177ad2b34413 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Wed, 2 Aug 2017 11:55:53 -0400 Subject: [PATCH 05/21] GPII-1939: New Platform Reporter reports screen resolutions Added machinery for providing a Linux/GNOME Platform Reporter that uses the Windows display settings handler to report all screen resolutions. Note: includes code from pull request for GPII-2404, "Windows display settings handler should report all available screen resolutions". --- .../platformReporter/src/PlatformReporter.js | 35 +++++++++++++++++ .../platformReporter/src/index.js | 19 ++++++++++ .../platformReporter/src/package.json | 19 ++++++++++ .../test/PlatformReporterTests.js | 38 +++++++++++++++++++ package.json | 2 +- tests/UnitTests.js | 1 + 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 gpii/node_modules/platformReporter/src/PlatformReporter.js create mode 100644 gpii/node_modules/platformReporter/src/index.js create mode 100644 gpii/node_modules/platformReporter/src/package.json create mode 100644 gpii/node_modules/platformReporter/test/PlatformReporterTests.js diff --git a/gpii/node_modules/platformReporter/src/PlatformReporter.js b/gpii/node_modules/platformReporter/src/PlatformReporter.js new file mode 100644 index 000000000..4df3ba6dd --- /dev/null +++ b/gpii/node_modules/platformReporter/src/PlatformReporter.js @@ -0,0 +1,35 @@ +/** + * GPII Windows Platform Reporter + * + * Copyright 2017 Inclusive Design Research Centre, OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/windows/blob/master/LICENSE.txt + * + * 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. + */ +"use strict"; + +var fluid = require("infusion"); +require("../../WindowsUtilities/WindowsUtilities.js"); +require("displaySettingsHandler"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.require("%universal"); + +fluid.defaults("gpii.platformReporter.windows", { + gradeNames: ["gpii.platformReporter"], + invokers: { + getOSspecifics: "gpii.platformReporter.windows.getOSspecifics" + } +}); + +gpii.platformReporter.windows.getOSspecifics = function () { + debugger; + return gpii.windows.display.getAllScreenResolutions(); +}; diff --git a/gpii/node_modules/platformReporter/src/index.js b/gpii/node_modules/platformReporter/src/index.js new file mode 100644 index 000000000..8239eea29 --- /dev/null +++ b/gpii/node_modules/platformReporter/src/index.js @@ -0,0 +1,19 @@ +/** + * GPII Windows Platform Reporter + * + * Copyright 2017 Inclusive Design Research Centre, OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/linux/blob/master/LICENSE.txt + * + * 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. + */ + +"use strict"; + +require("./PlatformReporter.js"); diff --git a/gpii/node_modules/platformReporter/src/package.json b/gpii/node_modules/platformReporter/src/package.json new file mode 100644 index 000000000..e7c234626 --- /dev/null +++ b/gpii/node_modules/platformReporter/src/package.json @@ -0,0 +1,19 @@ +{ + "name": "PlatformReporter", + "description": "The PlatformReporter is a Node.js add-on for reporting OS specific information.", + "version": "0.1.0", + "author": "Joseph Scheuhammer", + "bugs": "http://wiki.gpii.net/index.php/Main_Page", + "homepage": "http://gpii.net/", + "dependencies": {}, + "licenses": [ + { + "type": "BSD-3-Clause", + "url": "http://www.opensource.org/licenses/BSD-3-Clause" + } + ], + "keywords": ["gpii", "accessibility", "devices", "fluid"], + "repository": "git://github.com:GPII/linux.git", + "main": "./index.js", + "engines": { "node" : ">=0.8" } +} diff --git a/gpii/node_modules/platformReporter/test/PlatformReporterTests.js b/gpii/node_modules/platformReporter/test/PlatformReporterTests.js new file mode 100644 index 000000000..bd66e6a9c --- /dev/null +++ b/gpii/node_modules/platformReporter/test/PlatformReporterTests.js @@ -0,0 +1,38 @@ +/** + * GPII Windows Platform Reporter unit tests + * + * Copyright 2017 Inclusive Design Research Centre, OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/windows/blob/master/LICENSE.txt + * + * 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. + */ +"use strict"; + +var fluid = require("universal"), + os = require("os"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit"); + +require("../src/PlatformReporter.js"); +fluid.registerNamespace("gpii.platformReporter"); +fluid.registerNamespace("gpii.platformReporter.tests"); + +jqUnit.module("GPII windows Platform Reporter Module"); + +jqUnit.test("windows Platform Reporter", function () { + var platformReporter = gpii.platformReporter.windows(); + var report = platformReporter.reportPlatform(); + + jqUnit.assertNotNull("Platform Info", report); + jqUnit.assertEquals("OS", os.platform(), report.id); + jqUnit.assertEquals("version", os.release().replace("_", "-"), report.version); + jqUnit.assertNotNull("Screen resolution", report["screen-resolution"]); + jqUnit.assertNotNull("Available resolutions", report["available-resolution"]); +}); diff --git a/package.json b/package.json index 8c1d91733..1629e04e3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "ref": "1", "ref-struct": "1", "ref-array": "1.1.2", - "universal": "gpii/universal#b58d1859e4d53303a1fdc4d74640e0c43af345e5", + "universal": "klown/universal#GPII-1939", "nan": "amatas/nan#GPII-1929" }, "devDependencies": { diff --git a/tests/UnitTests.js b/tests/UnitTests.js index 2821f4c00..718dd3298 100644 --- a/tests/UnitTests.js +++ b/tests/UnitTests.js @@ -20,3 +20,4 @@ require("../gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js" require("../gpii/node_modules/processHandling/test/testProcessHandling"); require("../gpii/node_modules/registryResolver/test/testRegistryResolver.js"); require("../gpii/node_modules/registeredAT/test/testRegisteredAT.js"); +require("../gpii/node_modules/platformReporter/test/PlatformReporterTests.js"); From 79e745d4868493de051634cc167cb2f2b56362bd Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Thu, 3 Aug 2017 13:11:16 -0400 Subject: [PATCH 06/21] GPII-1939: Device Reporter reports screen resolutions Fixed bug so that GPII finds the Windows context platform reporter. --- .../platformReporter/{src => }/PlatformReporter.js | 3 +-- gpii/node_modules/platformReporter/{src => }/index.js | 0 gpii/node_modules/platformReporter/{src => }/package.json | 0 .../platformReporter/test/PlatformReporterTests.js | 2 +- index.js | 1 + 5 files changed, 3 insertions(+), 3 deletions(-) rename gpii/node_modules/platformReporter/{src => }/PlatformReporter.js (93%) rename gpii/node_modules/platformReporter/{src => }/index.js (100%) rename gpii/node_modules/platformReporter/{src => }/package.json (100%) diff --git a/gpii/node_modules/platformReporter/src/PlatformReporter.js b/gpii/node_modules/platformReporter/PlatformReporter.js similarity index 93% rename from gpii/node_modules/platformReporter/src/PlatformReporter.js rename to gpii/node_modules/platformReporter/PlatformReporter.js index 4df3ba6dd..78a0240a1 100644 --- a/gpii/node_modules/platformReporter/src/PlatformReporter.js +++ b/gpii/node_modules/platformReporter/PlatformReporter.js @@ -16,7 +16,7 @@ "use strict"; var fluid = require("infusion"); -require("../../WindowsUtilities/WindowsUtilities.js"); +require("../WindowsUtilities/WindowsUtilities.js"); require("displaySettingsHandler"); var gpii = fluid.registerNamespace("gpii"); @@ -30,6 +30,5 @@ fluid.defaults("gpii.platformReporter.windows", { }); gpii.platformReporter.windows.getOSspecifics = function () { - debugger; return gpii.windows.display.getAllScreenResolutions(); }; diff --git a/gpii/node_modules/platformReporter/src/index.js b/gpii/node_modules/platformReporter/index.js similarity index 100% rename from gpii/node_modules/platformReporter/src/index.js rename to gpii/node_modules/platformReporter/index.js diff --git a/gpii/node_modules/platformReporter/src/package.json b/gpii/node_modules/platformReporter/package.json similarity index 100% rename from gpii/node_modules/platformReporter/src/package.json rename to gpii/node_modules/platformReporter/package.json diff --git a/gpii/node_modules/platformReporter/test/PlatformReporterTests.js b/gpii/node_modules/platformReporter/test/PlatformReporterTests.js index bd66e6a9c..780cbc1cc 100644 --- a/gpii/node_modules/platformReporter/test/PlatformReporterTests.js +++ b/gpii/node_modules/platformReporter/test/PlatformReporterTests.js @@ -20,7 +20,7 @@ var fluid = require("universal"), gpii = fluid.registerNamespace("gpii"), jqUnit = fluid.require("node-jqunit"); -require("../src/PlatformReporter.js"); +require("../PlatformReporter.js"); fluid.registerNamespace("gpii.platformReporter"); fluid.registerNamespace("gpii.platformReporter.tests"); diff --git a/index.js b/index.js index 8b65210dc..fca26cf26 100644 --- a/index.js +++ b/index.js @@ -32,5 +32,6 @@ require("./gpii/node_modules/registrySettingsHandler"); require("./gpii/node_modules/registryResolver"); require("./gpii/node_modules/spiSettingsHandler"); require("./gpii/node_modules/registeredAT/registeredAT.js"); +require("./gpii/node_modules/platformReporter"); module.exports = fluid; From b3da5135155395440671868c423da8db48d446dc Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Thu, 3 Aug 2017 16:06:15 -0400 Subject: [PATCH 07/21] Merge branch 'master' into GPII-1939_GPII-2404 --- .../spiSettingsHandler/test/testAPI.json | 10 +-- .../spiSettingsHandler/test/testMouse.json | 2 +- listeners/GPII_RFIDListener/README.md | 2 +- .../GPII_RFIDListener/include/FlowManager.h | 9 +- .../GPII_RFIDListener/src/Diagnostic.cpp | 2 +- .../GPII_RFIDListener/src/FlowManager.cpp | 22 +++-- .../src/GPII_RFIDListener.cpp | 89 +++++-------------- .../src/GPII_RFIDListener.rc | 2 +- package.json | 3 +- 9 files changed, 48 insertions(+), 93 deletions(-) diff --git a/gpii/node_modules/spiSettingsHandler/test/testAPI.json b/gpii/node_modules/spiSettingsHandler/test/testAPI.json index b5e606b12..cdf9b7d41 100644 --- a/gpii/node_modules/spiSettingsHandler/test/testAPI.json +++ b/gpii/node_modules/spiSettingsHandler/test/testAPI.json @@ -76,7 +76,7 @@ }, "settings": { "MouseAcceleration": { - "value": 3, + "value": 0, "path": "pvParam.2" } } @@ -108,11 +108,11 @@ "settings": { "MouseAcceleration": { "oldValue": { - "value": 2, + "value": 1, "path": "pvParam.2" }, "newValue": { - "value": 3, + "value": 0, "path": "pvParam.2" } } @@ -156,7 +156,7 @@ }, "settings": { "MouseAcceleration": { - "value": 3, + "value": 1, "path": "pvParam.2" } } @@ -178,7 +178,7 @@ }, { "settings": { "MouseAcceleration": { - "value": 2, + "value": 1, "path": "pvParam.2" } } diff --git a/gpii/node_modules/spiSettingsHandler/test/testMouse.json b/gpii/node_modules/spiSettingsHandler/test/testMouse.json index 4cee3c0a0..ded3c72e3 100644 --- a/gpii/node_modules/spiSettingsHandler/test/testMouse.json +++ b/gpii/node_modules/spiSettingsHandler/test/testMouse.json @@ -12,7 +12,7 @@ "settings": { "MouseAcceleration": { - "value": 2, + "value": 0, "path": "pvParam.2" } } diff --git a/listeners/GPII_RFIDListener/README.md b/listeners/GPII_RFIDListener/README.md index 7ba57083a..7e50ff14d 100644 --- a/listeners/GPII_RFIDListener/README.md +++ b/listeners/GPII_RFIDListener/README.md @@ -1,6 +1,6 @@ # GPII RFIDListener -Windows executable that listens out for NFC RFID tags and invokes URLs to trigger GPII user log on/off. It listens out for RFID tag events and reads the encoded tag data. The listener tracks the login state and generate login and logout events on the GPII RESTful API (e.g http://localhost:8081/user/bert/login). +Windows executable that listens out for NFC RFID tags and invokes URLs to trigger GPII user log on/off. It listens out for RFID tag events and reads the encoded tag data. The listener does not track the login state and only informs GPII that a tag has been presented, via the GPII RESTful API (e.g http://localhost:8081/user/bert/proximityTriggered). See the [User listener](http://wiki.gpii.net/index.php/User_Listener) and [NFC](http://wiki.gpii.net/index.php/Using_the_NFC_Listener) pages on the GPII wikifor details of USB and NFC listening and how to encode user IDs. diff --git a/listeners/GPII_RFIDListener/include/FlowManager.h b/listeners/GPII_RFIDListener/include/FlowManager.h index a65488e53..705274ef5 100644 --- a/listeners/GPII_RFIDListener/include/FlowManager.h +++ b/listeners/GPII_RFIDListener/include/FlowManager.h @@ -24,7 +24,12 @@ #ifndef _FLOWMANAGER_H_ #define _FLOWMANAGER_H_ -void FlowManagerLogin(const char * szToken); -void FlowManagerLogout(const char * szToken); +// Also emit a "proximityRemoved" request. +//#define WANT_REMOVE_EVENT + +void FlowManagerCardOn(const char * szToken); +#ifdef WANT_REMOVE_EVENT +void FlowManagerCardOff(const char *szToken); +#endif #endif // _FLOWMANAGER_H_ diff --git a/listeners/GPII_RFIDListener/src/Diagnostic.cpp b/listeners/GPII_RFIDListener/src/Diagnostic.cpp index 5b16a4ac2..30d7e834b 100644 --- a/listeners/GPII_RFIDListener/src/Diagnostic.cpp +++ b/listeners/GPII_RFIDListener/src/Diagnostic.cpp @@ -173,7 +173,7 @@ LRESULT CALLBACK _WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) switch (message) { //----------------------------------------------------------- - // Tray Icon Menu Commands to Show, Hide, Exit or Logout + // Tray Icon Menu Commands to Show, Hide, or Exit //----------------------------------------------------------- case WM_MYLOG: { diff --git a/listeners/GPII_RFIDListener/src/FlowManager.cpp b/listeners/GPII_RFIDListener/src/FlowManager.cpp index 02e5f827d..1b2eb5a69 100644 --- a/listeners/GPII_RFIDListener/src/FlowManager.cpp +++ b/listeners/GPII_RFIDListener/src/FlowManager.cpp @@ -41,8 +41,8 @@ extern void WINAPI OutputDebugString( // Flow Manager Constants //--------------------------------------------------------- const char * const FLOW_MANAGER_URL = "http://localhost:8081/user"; -const char * const FLOW_LOGIN = "login"; -const char * const FLOW_LOGOUT = "logout"; +const char * const FLOW_CARDON = "proximityTriggered"; +const char * const FLOW_CARDOFF = "proximityRemoved"; /////////////////////////////////////////////////////////////////////////////// // @@ -50,11 +50,9 @@ const char * const FLOW_LOGOUT = "logout"; // // PURPOSE: Uses libcurl to make a HTTP GET request to a specified URL. // -// EXAMPLES: +// EXAMPLE: // -// http://localhost:8081/user/123/login -// -// http://localhost:8081/user/123/logout +// http://localhost:8081/user/123/proximityTriggered // /////////////////////////////////////////////////////////////////////////////// static int _MakeCurlRequest(const char* szUser, const char* szAction) @@ -88,16 +86,16 @@ static int _MakeCurlRequest(const char* szUser, const char* szAction) return 0; } - - -void FlowManagerLogin(const char * szToken) +void FlowManagerCardOn(const char *szToken) { - _MakeCurlRequest(szToken, FLOW_LOGIN); + _MakeCurlRequest(szToken, FLOW_CARDON); } -void FlowManagerLogout(const char * szToken) // FIXME should we keep state or can we have multiple concurrent logins? +#ifdef WANT_REMOVE_EVENT +void FlowManagerCardOff(const char *szToken) { - _MakeCurlRequest(szToken, FLOW_LOGOUT); + _MakeCurlRequest(szToken, FLOW_CARDOFF); } +#endif // End of file diff --git a/listeners/GPII_RFIDListener/src/GPII_RFIDListener.cpp b/listeners/GPII_RFIDListener/src/GPII_RFIDListener.cpp index 62b2f132b..f342a5b51 100644 --- a/listeners/GPII_RFIDListener/src/GPII_RFIDListener.cpp +++ b/listeners/GPII_RFIDListener/src/GPII_RFIDListener.cpp @@ -10,8 +10,8 @@ // You may obtain a copy of the License at // https://github.com/gpii/windows/blob/master/LICENSE.txt // -// The research leading to these results has received funding from -// the European Union's Seventh Framework Programme (FP7/2007-2013) +// 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. // // The GPII RFID listener has the following features @@ -105,7 +105,6 @@ const int MY_SIZE_Y = 100; #define MY_SHOWSTATUS (WM_USER + 3) #define MY_SHOWDIAG (WM_USER + 4) #define MY_EXIT (WM_USER + 5) -#define MY_LOGOUT (WM_USER + 6) #define MY_TIMER (WM_USER + 7) //--------------------------------------------------------- @@ -113,7 +112,7 @@ const int MY_SIZE_Y = 100; //--------------------------------------------------------- static const int MAX_BUFFER = 256; static char m_szStatus[MAX_BUFFER]; // FIXME potential buffer overruns as restricted length string functions not used. -static char m_szUserID[MAX_BUFFER]; +static char m_szLatestUserID[MAX_BUFFER]; static char m_szReader[MAX_BUFFER]; static int m_nLogin = 0; @@ -149,7 +148,7 @@ int APIENTRY WinMain(HINSTANCE hInstance, //----------------------------------------------------- // Initialize global variables //----------------------------------------------------- - wsprintf(m_szUserID,"%s",""); + wsprintf(m_szLatestUserID,"%s",""); wsprintf(m_szReader,"%s",lpCmdLine); wsprintf(m_szStatus,"%s","Listening..."); @@ -243,18 +242,21 @@ BOOL MyTrayIcon(HWND hWnd) BOOL MyPopupMenu(HWND hWnd) { POINT p; + int ret; + GetCursorPos(&p); HMENU hPopupMenu = CreatePopupMenu(); const UINT fStatusChecked = (IsWindowVisible(hWnd)) ? MF_CHECKED : 0; InsertMenu(hPopupMenu, 0, MF_BYPOSITION | MF_STRING | fStatusChecked, MY_SHOWSTATUS, "View status window"); const UINT fDiagChecked = (Diagnostic_IsShowing()) ? MF_CHECKED : 0; InsertMenu(hPopupMenu, 2, MF_BYPOSITION | MF_STRING | fDiagChecked, MY_SHOWDIAG, "View Diagnostic Window"); - InsertMenu(hPopupMenu, 3, MF_BYPOSITION | MF_STRING, MY_LOGOUT, "Logout"); InsertMenu(hPopupMenu, 4, MF_BYPOSITION | MF_STRING, MY_EXIT, "Exit"); SetMenuItemBitmaps(hPopupMenu, MY_SHOWDIAG, MF_BYCOMMAND, NULL, NULL); SetForegroundWindow(hWnd); - return TrackPopupMenu(hPopupMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, p.x,p.y,0,hWnd, NULL); + ret = TrackPopupMenu(hPopupMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, p.x,p.y,0,hWnd, NULL); + DestroyMenu(hPopupMenu); + return ret; } @@ -341,51 +343,21 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) // Smart Card User Login //----------------------------------------------------------- case SMART_CARD_ARRIVE: - if (m_nLogin == 0) - { - //----------------------------------------------- - // login a new user - //----------------------------------------------- - WinSmartCardGetUser(m_szUserID,MAX_BUFFER); - wsprintf(m_szStatus,"%s %s","CARD LOGIN",m_szUserID); - InvalidateRect(hWnd,NULL,TRUE); - FlowManagerLogin(m_szUserID); - m_nLogin = SMART_CARD_ARRIVE; - } - else - { - char szThisUser[MAX_BUFFER]; - WinSmartCardGetUser(szThisUser,MAX_BUFFER); - if (lstrcmp(szThisUser,m_szUserID) == 0) - { - //----------------------------------------------- - // logout the user - //----------------------------------------------- - wsprintf(m_szStatus,"%s %s","CARD LOGOUT",m_szUserID); - FlowManagerLogout(m_szUserID); - wsprintf(m_szUserID,"%s",""); - InvalidateRect(hWnd,NULL,TRUE); - m_nLogin = 0; - } - else - { - //----------------------------------------------- - // logout the old user and login new user - //----------------------------------------------- - FlowManagerLogout(m_szUserID); - WinSmartCardGetUser(m_szUserID,MAX_BUFFER); - FlowManagerLogin(m_szUserID); - wsprintf(m_szStatus,"%s %s","CARD LOGIN",m_szUserID); - InvalidateRect(hWnd,NULL,TRUE); - } - } + WinSmartCardGetUser(m_szLatestUserID, MAX_BUFFER); + wsprintf(m_szStatus, "%s %s", "CARD ARRIVE", m_szLatestUserID); + InvalidateRect(hWnd, NULL, TRUE); + FlowManagerCardOn(m_szLatestUserID); + m_nLogin = SMART_CARD_ARRIVE; break; //----------------------------------------------------------- // Smart Card Removed //----------------------------------------------------------- case SMART_CARD_REMOVE: - wsprintf(m_szStatus,"%s","Listening..."); +#ifdef WANT_REMOVE_EVENT + FlowManagerCardOff(m_szLatestUserID); +#endif + wsprintf(m_szStatus, "%s", "Listening..."); InvalidateRect(hWnd,NULL,TRUE); break; @@ -416,7 +388,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; //----------------------------------------------------------- - // Tray Icon Menu Commands to Show, Hide, Exit or Logout + // Tray Icon Menu Commands to Show, Hide, or Exit //----------------------------------------------------------- case WM_COMMAND: { @@ -430,21 +402,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { DestroyWindow(hWnd); } - else if (cmd == MY_LOGOUT) - { - if (m_nLogin) - { - wsprintf(m_szStatus,"%s","MENU LOGOUT"); - FlowManagerLogout(m_szUserID); - wsprintf(m_szUserID,"%s",""); - m_nLogin = 0; - } - else - { - wsprintf(m_szStatus,"%s","NO USER TO LOGOUT"); - } - InvalidateRect(hWnd,NULL,TRUE); - } if (cmd == MY_SHOWDIAG) { Diagnostic_Show(!Diagnostic_IsShowing()); @@ -476,8 +433,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) WinSmartCardPolling() ? "ONLINE": "OFFLINE"); DrawText(hdc, sReading, (int) strlen(sReading), &rt, DT_CENTER); rt.top = rt.bottom*70/100; - wsprintf(sCurrent,"%s %s","CURRENT USER:", - lstrlen(m_szUserID) ? m_szUserID : "NONE"); + wsprintf(sCurrent,"%s %s","LATEST USER:", + lstrlen(m_szLatestUserID) ? m_szLatestUserID : "NONE"); (void) MultiByteToWideChar(CP_UTF8, 0, sCurrent, -1, wsCurrent, _countof(wsCurrent)); DrawTextW(hdc, wsCurrent, (int)wcslen(wsCurrent), &rt, DT_CENTER); @@ -509,10 +466,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) // Destroy the Window and Exit //----------------------------------------------------------- case WM_DESTROY: - if (m_nLogin) - { - FlowManagerLogout(m_szUserID); - } KillTimer(hWnd,MY_TIMER); Diagnostic_CleanUp(); PostQuitMessage(0); diff --git a/listeners/GPII_RFIDListener/src/GPII_RFIDListener.rc b/listeners/GPII_RFIDListener/src/GPII_RFIDListener.rc index 9e37be1a0..5f143b2be 100644 --- a/listeners/GPII_RFIDListener/src/GPII_RFIDListener.rc +++ b/listeners/GPII_RFIDListener/src/GPII_RFIDListener.rc @@ -15,7 +15,7 @@ // #include -#define VER_PRODUCTVERSION 1,3,0 +#define VER_PRODUCTVERSION 1,4,0 #define __str(a) #a #define _str(a) __str(a) diff --git a/package.json b/package.json index 1629e04e3..fa208b615 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "ref": "1", "ref-struct": "1", "ref-array": "1.1.2", - "universal": "klown/universal#GPII-1939", - "nan": "amatas/nan#GPII-1929" + "universal": "klown/universal#GPII-1939" }, "devDependencies": { "grunt": "1.0.1", From 73d727cc4fce14306c09dab8219cb3329eb9f282 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Thu, 24 Aug 2017 16:34:00 -0400 Subject: [PATCH 08/21] GPII-2404: Fixed lint error. --- .../displaySettingsHandler/src/displaySettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index 439f27a60..90d730f19 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -275,7 +275,7 @@ gpii.windows.display.setImpl = function (payload) { results["screen-resolution"] = { oldValue: oldRes, newValue: newRes }; } - + var targetDpi = payload.settings["screen-dpi"]; if (targetDpi) { var oldDpi = windows.display.getScreenDpi(); From 86e1750d13e9b84834f1e655198de8142591fc75 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Thu, 24 Aug 2017 17:02:10 -0400 Subject: [PATCH 09/21] GPII-2404: DeviceReporter reports available screen resolutions Modified PlatformReporter to remove the "dmPels" prefix on width and height as returned by the display settings handler. --- .../platformReporter/PlatformReporter.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gpii/node_modules/platformReporter/PlatformReporter.js b/gpii/node_modules/platformReporter/PlatformReporter.js index 78a0240a1..a140df2d5 100644 --- a/gpii/node_modules/platformReporter/PlatformReporter.js +++ b/gpii/node_modules/platformReporter/PlatformReporter.js @@ -30,5 +30,15 @@ fluid.defaults("gpii.platformReporter.windows", { }); gpii.platformReporter.windows.getOSspecifics = function () { - return gpii.windows.display.getAllScreenResolutions(); + var allResolutions = gpii.windows.display.getAllScreenResolutions(); + var rule = { + width: "dmPelsWidth", + height: "dmPelsHeight" + }; + var available = allResolutions["available-resolutions"] ; + available = fluid.transform (available, function (currRes) { + return fluid.model.transformWithRules (currRes, rule); + }); + allResolutions["available-resolutions"] = available; + return allResolutions; }; From c35c2c96513bce3c782720470e67baa141911e8e Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 25 Aug 2017 10:58:32 -0400 Subject: [PATCH 10/21] GPII-2404: Display settings handler returns all screen resolutions. Modifed the "getAllScreenResolutions" to drop the "dmPels" prefix. --- .../src/displaySettingsHandler.js | 11 +++++++-- .../test/testDisplaySettingsHandler.js | 23 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index 90d730f19..057e5f400 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -207,6 +207,13 @@ gpii.windows.display.getAllScreenResolutions = function () { var winDisplay = gpii.windowsDisplay(); var currentResolution = winDisplay.getScreenResolution(); var available = winDisplay.getAvailableResolutions(); + var rule = { + width: "dmPelsWidth", + height: "dmPelsHeight" + }; + available = fluid.transform (available, function (currRes) { + return fluid.model.transformWithRules (currRes, rule); + }); return { "screen-resolution": currentResolution, "available-resolutions": available @@ -288,10 +295,10 @@ gpii.windows.display.setImpl = function (payload) { }; /* - * Returns the display's current screen resolution. + * Returns the display's current screen resolution, and dpi. * * @return {Object} containing "screen-resolution" property that is the current - * resolution {width, height}, and an "screen-dpi" property. + * resolution {width, height}, and a "screen-dpi" property. */ gpii.windows.display.getImpl = function () { var results = {}; diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index a32425ea0..f09c1a582 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -140,8 +140,19 @@ jqUnit.test("Get all screen resolutions", function () { var all = windowsDisplay.getAllScreenResolutions(); jqUnit.assertDeepEq("All screen resolutions contains current resolution", current, all["screen-resolution"]); - jqUnit.assertDeepEq("All screen resolutions contains available resolution", - available, all["available-resolutions"]); + + // Revisit use of "dmPels" prefix as returnsd by getAvailalbeResolutions(). + // Can't do a deep equals because the property names are different. + var allAvailable = all["available-resolutions"]; + jqUnit.assertEquals("All screen resolutions contain available resolutions", + available.length, allAvailable.length); + fluid.each (available, function (expected, index) { + var actual = allAvailable[index]; + jqUnit.assertEquals("Available resolution width in 'all' structure vs available", + expected.dmPelsWidth, actual.width); + jqUnit.assertEquals("Available resolution height in 'all' structure vs available", + expected.dmPelsHeight, actual.height); + }); }); jqUnit.test("Valid and invalid screen resolutions", function () { @@ -209,15 +220,15 @@ jqUnit.test("Windows display settings handlerr: getImpl()", function () { jqUnit.test("Windows display settings handler: setImpl()", function () { var allRez = windowsDisplay.getAllScreenResolutions(); var newRez = fluid.find(allRez["available-resolutions"], function (aRez) { - if (aRez.dmPelsWidth !== allRez["screen-resolution"].width || - aRez.dmPelsHeight !== allRez["screen-resolution"].height) { + if (aRez.width !== allRez["screen-resolution"].width || + aRez.height !== allRez["screen-resolution"].height) { return aRez; } }, null); if (newRez !== null) { var rezToSet = {}; - rezToSet.width = newRez.dmPelsWidth; - rezToSet.height = newRez.dmPelsHeight; + rezToSet.width = newRez.width; + rezToSet.height = newRez.height; var screenResolution = {}; fluid.set(screenResolution, "screen-resolution", rezToSet); var payload = {}; From f9778e3452f7414263e96b8c861008e8e8048aa7 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 25 Aug 2017 11:39:50 -0400 Subject: [PATCH 11/21] GPII-2404: Fixed lint error. --- .../displaySettingsHandler/test/testDisplaySettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index f09c1a582..d0418ec56 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -140,7 +140,7 @@ jqUnit.test("Get all screen resolutions", function () { var all = windowsDisplay.getAllScreenResolutions(); jqUnit.assertDeepEq("All screen resolutions contains current resolution", current, all["screen-resolution"]); - + // Revisit use of "dmPels" prefix as returnsd by getAvailalbeResolutions(). // Can't do a deep equals because the property names are different. var allAvailable = all["available-resolutions"]; From c15a94741b0dc6e1e8fb689659392144cd7b640b Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Mon, 11 Sep 2017 11:08:22 -0400 Subject: [PATCH 12/21] GPII-1939: Merge upstream GPII master branch into GPII-1939 --- .../WindowsUtilities/WindowsUtilities.js | 69 ++ .../processHandling/processHandling.js | 114 ++- .../test/testProcessHandling.js | 63 ++ .../processReporter/dotNetProcesses.csx | 143 +++ gpii/node_modules/processReporter/index.js | 16 + .../node_modules/processReporter/package.json | 19 + .../processReporter/processReporter.js | 42 + .../processReporter/processesBridge.js | 51 + .../processReporter/test/all-tests.js | 28 + .../test/processReporterModuleTests.js | 43 + .../test/processesBridge_tests.js | 225 +++++ .../src/RegistrySettingsHandler.js | 55 +- .../test/testRegistrySettingsHandler.js | 53 +- gpii/node_modules/windowsMetrics/index.js | 25 + gpii/node_modules/windowsMetrics/package.json | 13 + .../windowsMetrics/src/windowsMetrics.js | 463 +++++++++ .../test/WindowsMetricsTests.js | 875 ++++++++++++++++++ index.js | 2 + package.json | 5 +- tests/UnitTests.js | 2 + 20 files changed, 2275 insertions(+), 31 deletions(-) create mode 100644 gpii/node_modules/processReporter/dotNetProcesses.csx create mode 100644 gpii/node_modules/processReporter/index.js create mode 100644 gpii/node_modules/processReporter/package.json create mode 100644 gpii/node_modules/processReporter/processReporter.js create mode 100644 gpii/node_modules/processReporter/processesBridge.js create mode 100644 gpii/node_modules/processReporter/test/all-tests.js create mode 100644 gpii/node_modules/processReporter/test/processReporterModuleTests.js create mode 100644 gpii/node_modules/processReporter/test/processesBridge_tests.js create mode 100644 gpii/node_modules/windowsMetrics/index.js create mode 100644 gpii/node_modules/windowsMetrics/package.json create mode 100644 gpii/node_modules/windowsMetrics/src/windowsMetrics.js create mode 100644 gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js diff --git a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js index 66f066256..719b6700f 100644 --- a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js +++ b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js @@ -105,6 +105,14 @@ windows.kernel32 = ffi.Library("kernel32", { // https://msdn.microsoft.com/en-us/library/windows/desktop/ms684139.aspx "IsWow64Process": [ t.BOOL, [t.INT, t.PBOOL ] + ], + // https://msdn.microsoft.com/library/ms684919.aspx + "QueryFullProcessImageNameW": [ + t.BOOL, [t.HANDLE, t.DWORD, "char*", t.LPDWORD ] + ], + // https://msdn.microsoft.com/library/ms683199 + "GetModuleHandleW": [ + t.HANDLE, ["int"] ] }); @@ -121,6 +129,26 @@ windows.user32 = ffi.Library("user32", { "PostMessageW": [ t.BOOL, [t.HANDLE, t.UINT, t.UINT, t.HANDLE ] ], + // https://msdn.microsoft.com/library/ms633505 + "GetForegroundWindow": [ + t.HANDLE, [] + ], + // https://msdn.microsoft.com/library/ms644990 + "SetWindowsHookExW": [ + t.HANDLE, [ t.INT, "void*", t.HANDLE, t.DWORD ] + ], + // https://msdn.microsoft.com/library/ms644993 + "UnhookWindowsHookEx": [ + t.BOOL, [ t.HANDLE ] + ], + // https://msdn.microsoft.com/library/ms644974 + "CallNextHookEx": [ + t.HANDLE, [ t.HANDLE, "int", t.HANDLE, t.PVOID ] + ], + // https://msdn.microsoft.com/library/ms646306 + "MapVirtualKeyW": [ + t.UINT, [ t.UINT, t.UINT ] + ], // https://msdn.microsoft.com/en-us/library/dd145064.aspx "MonitorFromWindow": [ t.HANDLE, [ t.HANDLE, t.DWORD ] @@ -153,6 +181,8 @@ windows.API_constants = { CP_UTF8: 65001, KEY_QUERY_VALUE: 1, KEY_SET_VALUE: 2, + KEY_WOW64_32KEY: 0x200, + KEY_WOW64_64KEY : 0x100, returnCodesLookup: { 0: "ERROR_SUCCESS", 1: "ERROR_INVALID_FUNCTION", @@ -165,6 +195,7 @@ 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, // http://stackoverflow.com/questions/23452271/is-max-path-always-same-size-even-if-unicode-macro-is-defined MAX_PATH: 260, @@ -176,6 +207,23 @@ windows.API_constants = { TRUE: 1, // https://msdn.microsoft.com/library/windows/desktop/ms632641 WM_QUIT: 0x12, + // https://msdn.microsoft.com/library/ms646281 + WM_KEYUP: 0x101, + + // https://msdn.microsoft.com/library/dd375731 + VK_BACK: 0x08, + VK_ESCAPE: 0x1B, + VK_HOME: 0x24, + VK_LEFT: 0x25, + VK_DELETE: 0x2E, + + LLKHF_EXTENDED: 0x01, + LLKHF_INJECTED: 0x10, + LLKHF_ALTDOWN: 0x20, + LLKHF_UP: 0x80, + + // https://msdn.microsoft.com/library/ms646306 + MAPVK_VK_TO_CHAR: 2, // The AccessibilityTemp values; https://msdn.microsoft.com/library/windows/desktop/bb879984.aspx disableAT: 2, @@ -307,6 +355,16 @@ windows.MouseKeysPointer = ref.refType(windows.MouseKeys); // TODO Define additional structures used in calls to SystemParametersInfo here. +// https://msdn.microsoft.com/library/ms644967 +windows.KBDLLHookStruct = new Struct([ + [t.DWORD, "vkCode"], + [t.DWORD, "scanCode"], + [t.DWORD, "flags"], + [t.DWORD, "time"], + [t.ULONG_PTR, "dwExtraInfo"] +]); +windows.KBDLLHookStructPointer = ref.refType(windows.KBDLLHookStruct); + /** * Contains actions that can be used as the first argument of the SystemParametersInfo function. */ @@ -702,6 +760,17 @@ windows.isWow64 = function () { return !!ptr.deref(); }; +/** + * Gets the pid of the process that owns the given window. + * @param hwnd The window handle. + * @return {int} The process ID. + */ +windows.getWindowProcessId = function (hwnd) { + var ptr = ref.alloc(windows.types.DWORD); + windows.user32.GetWindowThreadProcessId(hwnd, ptr); + return ptr.deref(); +}; + /** * Waits for a condition, by polling a given function. A promise is returned, resolving when the condition is met or * rejecting upon timeout. If the condition is already met, then the returned promise will be resolved. diff --git a/gpii/node_modules/processHandling/processHandling.js b/gpii/node_modules/processHandling/processHandling.js index b8fa6a5bc..d8b196580 100644 --- a/gpii/node_modules/processHandling/processHandling.js +++ b/gpii/node_modules/processHandling/processHandling.js @@ -54,11 +54,12 @@ gpii.windows.killProcessByName = function (filename) { * The CreateToolhelp32Snapshot Windows API call captures the running processes, and the Process32First/Next * functions are used to enumerate them. * - * @param filename The exe file name to search for. + * @param filename The exe file name to search for, null to match any. * @param all {boolean?} Set to true to return an array containing all matching processes. + * @param fullInfo {boolean?} Set to true to return the pid, ppid, and exe name of matching processes. * @returns {?number|number[]} The Process ID of the matching processes, otherwise null. */ -gpii.windows.findProcessByName = function (filename, all) { +gpii.windows.findProcessByName = function (filename, all, fullInfo) { // Get a snapshot of the processes. var hSnapShot = windows.kernel32.CreateToolhelp32Snapshot(windows.API_constants.TH32CS_SNAPPROCESS, null); @@ -68,7 +69,7 @@ gpii.windows.findProcessByName = function (filename, all) { } var matches = []; - var filenameLower = filename.toLowerCase(); + var filenameLower = filename && filename.toLowerCase(); try { // Create the structure for the return parameter of Process32First/Next. @@ -81,13 +82,23 @@ gpii.windows.findProcessByName = function (filename, all) { var buf = new Buffer(pEntry.szExeFile); var processName = ref.readCString(buf, 0); - if (processName.toLowerCase() === filenameLower) { + if (!filename || processName.toLowerCase() === filenameLower) { + var proc; + if (fullInfo) { + proc = { + pid: pEntry.th32ProcessID, + ppid: pEntry.th32ParentProcessID, + exeFile: processName + }; + } else { + proc = pEntry.th32ProcessID; + } if (all) { // Add it to the array of matches. - matches.push(pEntry.th32ProcessID); + matches.push(proc); } else { // Only want the first one - return it. - return pEntry.th32ProcessID; + return proc; } } @@ -106,11 +117,22 @@ gpii.windows.findProcessByName = function (filename, all) { /** * Determines if a given process is running, returning true if it is. * - * @param {string} filename The name of the executable. + * @param proc {string|number} The name of the executable, or the process ID. * @return {boolean} true if the process is running. */ -gpii.windows.isProcessRunning = function (filename) { - return gpii.windows.findProcessByName(filename) !== null; +gpii.windows.isProcessRunning = function (proc) { + var togo = false; + if (isNaN(proc)) { + togo = gpii.windows.findProcessByName(proc) !== null; + } else { + try { + process.kill(proc, 0); + togo = true; + } catch (e) { + togo = false; + } + } + return togo; }; @@ -120,7 +142,7 @@ gpii.windows.isProcessRunning = function (filename) { * * The promise will reject if the process has yet to become in the desired state after the timeout. * - * @param filename The executable. + * @param proc {string|number} The name of the executable, or the process ID. * @param options The options. * @param options.start The desired state: true to wait for the process to start, false to wait for the termination. * @param options.timeout Approximate milliseconds to wait, or null for infinite. @@ -128,7 +150,7 @@ gpii.windows.isProcessRunning = function (filename) { * @return {promise} The promise will resolve when the process is in the desired state, or will reject if after the * timeout the process is still in the same state. */ -gpii.windows.waitForProcessState = function (filename, options) { +gpii.windows.waitForProcessState = function (proc, options) { var defaultOptions = { pollDelay: 500, timeout: null, @@ -138,13 +160,13 @@ gpii.windows.waitForProcessState = function (filename, options) { options = fluid.extend(true, defaultOptions, options); var waitOptions = { - argument: filename, + argument: proc, conditionValue: options.start, pollDelay: options.pollDelay, timeout: options.timeout, error: { isError: true, - message: "Timed out waiting for process " + filename + + message: "Timed out waiting for process " + proc + " to " + (options.start ? "start" : "terminate") + " after " + options.timeout + "ms" } }; @@ -157,16 +179,16 @@ gpii.windows.waitForProcessState = function (filename, options) { * running. If there are no processes running when the function is called, the promise will already be resolved. * The promise will reject if the process is still running after the timeout. * - * @param filename The executable. + * @param proc {string|number} The name of the executable, or the process ID. * @param userOptions The options. * @param userOptions.timeout Approximate milliseconds to wait, or null for infinite. * @param userOptions.pollDelay How long to wait, in milliseconds, between checking for the process. * @return {promise} The promise will resolve when there are no matching processes running, or will reject if a matching * process is still running after the timeout. */ -gpii.windows.waitForProcessTermination = function (filename, userOptions) { +gpii.windows.waitForProcessTermination = function (proc, userOptions) { var options = fluid.extend(true, {start: false}, userOptions); - return gpii.windows.waitForProcessState(filename, options); + return gpii.windows.waitForProcessState(proc, options); }; /** @@ -174,16 +196,16 @@ gpii.windows.waitForProcessTermination = function (filename, userOptions) { * If there are already processes running when the function is called, the promise will already be resolved. * The promise will reject if a matching process is still not running after the timeout. * - * @param filename The executable. + * @param proc {string|number} The name of the executable, or the process ID. * @param userOptions The options. * @param userOptions.timeout Approximate milliseconds to wait, or null for infinite. * @param userOptions.pollDelay How long to wait, in milliseconds, between checking for the process. * @return {promise} The promise will resolve when there is a matching processes running, or will reject if a matching * process is still not running after the timeout. */ -gpii.windows.waitForProcessStart = function (filename, userOptions) { +gpii.windows.waitForProcessStart = function (proc, userOptions) { var options = fluid.extend(true, {start: true}, userOptions); - return gpii.windows.waitForProcessState(filename, options); + return gpii.windows.waitForProcessState(proc, options); }; /** @@ -274,6 +296,60 @@ gpii.windows.closeProcessByName = function (filename, options) { return promiseTogo; }; +/** + * Gets the path to the executable of a running process. + * + * Provide the processHandle if it's available when calling, otherwise provide the pid. + * + * @param pid {Number} [Optional] The process ID. + * @param processHandle [Optional] The process handle. + * @return {String} The path to the executable. + */ +gpii.windows.getProcessPath = function (pid, processHandle) { + var hProcess; + var togo = null; + + if (processHandle) { + hProcess = processHandle; + } else if (pid) { + hProcess = + gpii.windows.kernel32.OpenProcess(gpii.windows.API_constants.PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); + } else { + fluid.fail("Either pid or processHandle needs to be given."); + } + + if (hProcess) { + try { + var size = ref.alloc(gpii.windows.types.DWORD); + size.writeUInt32LE(gpii.windows.API_constants.MAX_PATH, 0); + var path = new Buffer(gpii.windows.API_constants.MAX_PATH); + + var success = gpii.windows.kernel32.QueryFullProcessImageNameW(hProcess, 0, path, size); + if (success) { + togo = gpii.windows.fromWideChar(path); + } + } finally { + if (hProcess !== processHandle) { + gpii.windows.kernel32.CloseHandle(hProcess); + } + } + } + + if (!togo) { + // OpenProcess or QueryFullProcessImageName failed (usually due to permissions). Get the file name from the + // process list instead. + var all = gpii.windows.findProcessByName(null, true, true); + var process = all.find(function (p) { + return p.pid === pid; + }); + if (process) { + togo = process.exeFile; + } + } + return togo; + +}; + fluid.defaults("gpii.windows.killProcessByName", { gradeNames: "fluid.function", argumentMap: { diff --git a/gpii/node_modules/processHandling/test/testProcessHandling.js b/gpii/node_modules/processHandling/test/testProcessHandling.js index 99a88ff1e..fb2a00220 100644 --- a/gpii/node_modules/processHandling/test/testProcessHandling.js +++ b/gpii/node_modules/processHandling/test/testProcessHandling.js @@ -55,11 +55,40 @@ jqUnit.test("Testing findProcessByName", function () { pid = gpii.windows.findProcessByName("node.exe"); jqUnit.assertNotEquals("Process should have been found running", null, pid); + // Find a process that is running, full details + var details = gpii.windows.findProcessByName("node.exe", false, true); + jqUnit.assertNotEquals("Process should have been found running (fullInfo)", null, details); + jqUnit.assertTrue("pid should be a number", typeof(details.pid) === "number"); + jqUnit.assertTrue("ppid should be a number", typeof(details.ppid) === "number"); + jqUnit.assertEquals("exeFile should be node.exe", "node.exe", details.exeFile.toLowerCase()); + // Find multiple processes. There's always more than one svchost.exe running on Windows. var pids = gpii.windows.findProcessByName("svchost.exe", true); jqUnit.assertTrue("Should have found several matching process.", pids.length > 1); }); +jqUnit.test("Testing isProcessRunning", function () { + // Check a PID that isn't running. + var running = gpii.windows.isProcessRunning(-1); + jqUnit.assertFalse("Process should not have been found running", running); + + // Check a PID that is running. + running = gpii.windows.isProcessRunning(process.pid); + jqUnit.assertTrue("Process should have been found running", running); + + // Check an exe file that isn't running. + running = gpii.windows.isProcessRunning("a non-matching process.exe"); + jqUnit.assertFalse("Process should not have been found running", running); + + // Check an exe file that is running. + running = gpii.windows.isProcessRunning("node.exe"); + jqUnit.assertTrue("Process should have been found running", running); + + // Check multiple processes. There's always more than one svchost.exe running on Windows. + running = gpii.windows.isProcessRunning("svchost.exe", true); + jqUnit.assertTrue("Process should have been found running (multiple processes)", running); +}); + jqUnit.asyncTest("Testing timeout waiting for processes start", function () { jqUnit.expect(1); @@ -207,3 +236,37 @@ jqUnit.asyncTest("Testing closeProcessByName (window-less process)", function () fluid.log("Executing " + command); child_process.exec(command); }); + +jqUnit.asyncTest("Testing getProcessPath", function () { + jqUnit.expect(3); + + var exeName = waitExe; + var exePath = waitExePath; + var args = [ "getProcessPathTest", "/T", "30"]; + + fluid.log("Executing " + exePath); + var child = child_process.spawn(exePath, args); + + gpii.windows.waitForProcessStart(exeName) + .then(function () { + jqUnit.assert("Process started"); + + // Test with the PID. + var result1 = gpii.windows.getProcessPath(child.pid); + jqUnit.assertEquals("The exe path should be correct (pid)", exePath, result1); + + // Test with the process handle. + var hProcess = gpii.windows.kernel32.OpenProcess( + gpii.windows.API_constants.PROCESS_QUERY_LIMITED_INFORMATION, 0, child.pid); + + try { + var result2 = gpii.windows.getProcessPath(null, hProcess); + jqUnit.assertEquals("The exe path should be correct (processHandle)", exePath, result2); + } finally { + gpii.windows.kernel32.CloseHandle(hProcess); + } + + child.kill(); + }); + child.on("close", jqUnit.start); +}); diff --git a/gpii/node_modules/processReporter/dotNetProcesses.csx b/gpii/node_modules/processReporter/dotNetProcesses.csx new file mode 100644 index 000000000..7bc9dc146 --- /dev/null +++ b/gpii/node_modules/processReporter/dotNetProcesses.csx @@ -0,0 +1,143 @@ +/*! +GPII Process Reporter processes bridge -- gpii.processes. + +Copyright 2016 Inclusive Design Research Centre, OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Dynamic; +using System.Management; + +public class ProcInfo +{ + public string command = ""; + public int pid = -1; + public string fullPath = ""; + public string[] argv; + public string state = ""; +} + +public class Startup +{ + public async Task Invoke(dynamic input) + { + ManagementObject[] processes = null; + object propValue; + ProcessModule mainModule; + ProcInfo aProcInfo; + ArrayList result; + + ManagementObjectCollection moc = new ManagementClass(new ManagementPath ("Win32_Process")).GetInstances(); + processes = new ManagementObject[moc.Count]; + moc.CopyTo(processes, 0); + result = new ArrayList(); + + // Check input for specified process id or command name. + if (input != null) { + if (input.GetType() == typeof(System.Int32)) { + ManagementObject aProcess = Array.Find( + processes, p => Convert.ToInt32(p.GetPropertyValue("ProcessId")) == input + ); + if (aProcess != null) { + result.Add(makeProcInfo(aProcess)); + } + } else { + ManagementObject[] someProcesses = Array.FindAll( + processes, p => p.GetPropertyValue("Name").ToString() == input + ); + foreach (ManagementObject p in someProcesses) { + result.Add(makeProcInfo(p)); + } + } + } + // No process specified; gat all processes and their info + else { + foreach (ManagementObject p in processes) { + result.Add(makeProcInfo(p)); + } + } + return result.ToArray(); + } + + public static ProcInfo makeProcInfo(ManagementObject process) { + object propValue; + + ProcInfo procInfo = new ProcInfo(); + procInfo.command = process.GetPropertyValue("Name").ToString(); + procInfo.pid = Convert.ToInt32(process.GetPropertyValue("ProcessId")); + + propValue = process.GetPropertyValue("ExecutablePath"); + if (propValue != null) { + procInfo.fullPath = propValue.ToString(); + } + propValue = process.GetPropertyValue("CommandLine"); + if (propValue != null) { + procInfo.argv = makeArgv(propValue.ToString()); + } + // Should be able to get the state of the process using the + // "ExecutionState" property, but that is not implemented; see: + // http://maestriatech.com/wmi.php + // + // Use the Process's main thread's state (the first one). + // Also, map Windows thread states to Linux-y process states. + Process theProcess = Process.GetProcessById(procInfo.pid); + switch (theProcess.Threads[0].ThreadState) { + case ThreadState.Running: + case ThreadState.Wait: + procInfo.state = "Running"; + break; + + case ThreadState.Initialized: + case ThreadState.Ready: + case ThreadState.Standby: + case ThreadState.Transition: + procInfo.state = "Sleeping"; + break; + + case ThreadState.Terminated: + case ThreadState.Unknown: + procInfo.state = "Zombie"; + break; + } + return procInfo; + } + + public static String[] makeArgv(String commandLine) + { + List arguments = new List(); + + commandLine = commandLine + " "; + for (int c = 0; c < commandLine.Length; c++) { + // Handle quoted strings. + if (commandLine[c] == '"') { + int firstChar = c + 1; + int endQuoteIndex = commandLine.IndexOf('"', firstChar); + arguments.Add(commandLine.Substring(firstChar, endQuoteIndex - firstChar)); + c = endQuoteIndex; + } + // Skip spaces outside of quoted strings. + else if (commandLine[c] == ' ') { + continue; + } + // Handle non-quoted string. + else { + int firstChar = c; + int endStringIndex = commandLine.IndexOf(' ', firstChar); + arguments.Add(commandLine.Substring(firstChar, endStringIndex - firstChar)); + c = endStringIndex; + } + } + return arguments.ToArray(); + } +} diff --git a/gpii/node_modules/processReporter/index.js b/gpii/node_modules/processReporter/index.js new file mode 100644 index 000000000..23aa61404 --- /dev/null +++ b/gpii/node_modules/processReporter/index.js @@ -0,0 +1,16 @@ +/** + * GPII Windows Process Reporter + * + * Copyright 2016 Inclusive Design Research Centre, OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/gpii/universal/LICENSE.txt + */ + +"use strict"; + +require("./processReporter.js"); + diff --git a/gpii/node_modules/processReporter/package.json b/gpii/node_modules/processReporter/package.json new file mode 100644 index 000000000..f6a4ba1a4 --- /dev/null +++ b/gpii/node_modules/processReporter/package.json @@ -0,0 +1,19 @@ +{ + "name": "processReporter", + "description": "The Process Reporter module provides information about running solutions on a platform", + "version": "0.1.0", + "author": "Joseph Scheuhammer", + "bugs": "http://wiki.gpii.net/index.php/Main_Page", + "homepage": "http://gpii.net/", + "dependencies": {}, + "licenses": [ + { + "type": "BSD-3-Clause", + "url": "http://www.opensource.org/licenses/BSD-3-Clause" + } + ], + "keywords": ["gpii", "accessibility", "processes", "fluid", "windows"], + "repository": "git://github.com:GPII/windows.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.18" } +} diff --git a/gpii/node_modules/processReporter/processReporter.js b/gpii/node_modules/processReporter/processReporter.js new file mode 100644 index 000000000..dbf43ca6a --- /dev/null +++ b/gpii/node_modules/processReporter/processReporter.js @@ -0,0 +1,42 @@ +/** + * GPII Process Reporter Bridge (Linux -- GLiBTop). + * + * Copyright 2016-2017 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/gpii/universal/LICENSE.txt + */ + +"use strict"; + +var fluid = require("universal"); +var gpii = fluid.registerNamespace("gpii"); +require("./processesBridge.js"); + +var processesBridge = gpii.processes.windows(); + +fluid.registerNamespace("gpii.processReporter"); +fluid.defaults("gpii.processReporter.find", { + gradeNames: "fluid.function", + argumentMap: { + command: 0 + } +}); + +// Search for the process using its command name. Returns a boolean indicating +// if the process is running (and if the setting is set). +gpii.processReporter.find = function (commandName) { + var running = false; + var theProcess = null; + var procInfos = processesBridge.findSolutionsByCommands([commandName]); + if (procInfos.length > 0) { + theProcess = procInfos[0]; + } + if (theProcess !== null) { + running = processesBridge.isRunning(theProcess.state); + } + return running; +}; diff --git a/gpii/node_modules/processReporter/processesBridge.js b/gpii/node_modules/processReporter/processesBridge.js new file mode 100644 index 000000000..91458a24b --- /dev/null +++ b/gpii/node_modules/processReporter/processesBridge.js @@ -0,0 +1,51 @@ +/*! +GPII Process Reporter processes bridge -- gpii.processes. + +Copyright 2016-2017 Inclusive Design Research Centre, OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +/*global require */ + +"use strict"; + +var path = require("path"), + // This conditional require is to run the electron version of edge.js + // when this code runs on electron (gpii-app). + edge = process.versions.electron ? require("electron-edge") : require("edge"), + fluid = require("universal"); + +var gpii = fluid.registerNamespace("gpii"); + +var dotNetProcesses = edge.func({ + source: path.join(__dirname, "dotNetProcesses.csx"), + references: ["System.Management.dll"] +}); + +fluid.defaults("gpii.processes.windows", { + gradeNames: ["gpii.processes"], + invokers: { + getProcessList: { + funcName: "gpii.processes.windows.getProcessList", + args: ["{arguments}.0"] + // process name or id, optional. + } + } +}); + +/** + * Return a list of processes -- a snapshot of the current processes. If the + * process name or process id is passed, then the return value will be limited + * to only processes with that name or the process with that pid. + * + * @param {String} or {Number} Optional. Specify a process to find via a name + * (string), or a process id (number). + */ +gpii.processes.windows.getProcessList = function (processIdentifier) { + return dotNetProcesses(processIdentifier, true); +}; diff --git a/gpii/node_modules/processReporter/test/all-tests.js b/gpii/node_modules/processReporter/test/all-tests.js new file mode 100644 index 000000000..99db881f9 --- /dev/null +++ b/gpii/node_modules/processReporter/test/all-tests.js @@ -0,0 +1,28 @@ +/** + * GPII PackageKit Device Reporter Tests + * + * Copyright 2016 Inclusive Design Research Centre, OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/gpii/universal/LICENSE.txt + */ +"use strict"; + +var fluid = require("universal"), + kettle = fluid.require("kettle"); + +kettle.loadTestingSupport(); + +var testIncludes = [ + "./processesBridge_tests.js", + "./processReporterModuleTests.js" +]; + +var tests = []; + +fluid.each(testIncludes, function (path) { + tests = tests.concat(fluid.require(path, require)); +}); diff --git a/gpii/node_modules/processReporter/test/processReporterModuleTests.js b/gpii/node_modules/processReporter/test/processReporterModuleTests.js new file mode 100644 index 000000000..d379fe22c --- /dev/null +++ b/gpii/node_modules/processReporter/test/processReporterModuleTests.js @@ -0,0 +1,43 @@ +/** + * GPII Process Reporter Tests + * + * Copyright 2015-2017 Inclusive Design Research Centre, OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/gpii/universal/LICENSE.txt + */ +"use strict"; + +var fluid = require("universal"), + jqUnit = fluid.require("node-jqunit"); + +require("processReporter"); + +var gpii = fluid.registerNamespace("gpii"); +var processReporter = fluid.registerNamespace("gpii.processReporter"); + +jqUnit.module("GPII Windows ProcessReporter Module"); + +jqUnit.test("Running tests for Windows Process Reporter", function () { + jqUnit.expect(3); + + // Check that the bridge is loaded and required methods are available + // + var methods = ["find"]; + for (var method in methods) { + jqUnit.assertTrue("Checking availability of method '" + method + "'", + (methods[method] in processReporter)); + } + + // This is running inside 'node' itself, so the uid matches. + jqUnit.assertTrue("Checking run-status of process 'node'", + gpii.processReporter.find("node.exe")); + + // Unlikely there is ever a process named "T6y7u8i9C". + jqUnit.assertFalse("Checking run-status of process 'T6y7u8i9'", + gpii.processReporter.find("T6y7u8i9C")); + +}); diff --git a/gpii/node_modules/processReporter/test/processesBridge_tests.js b/gpii/node_modules/processReporter/test/processesBridge_tests.js new file mode 100644 index 000000000..dd1d30d30 --- /dev/null +++ b/gpii/node_modules/processReporter/test/processesBridge_tests.js @@ -0,0 +1,225 @@ +/*! +GPII Node.js Processes Bridge Unit Tests + +Copyright 2014-2017 Inclusive Design Research Centre, OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +/*global require*/ + +"use strict"; + +var path = require("path"), + spawn = require("child_process").spawn, + fluid = require("universal"), + jqUnit = fluid.require("node-jqunit"); + +require("../processesBridge.js"); + +var gpii = fluid.registerNamespace("gpii"); +var procTests = fluid.registerNamespace("gpii.tests.processes"); + +// Delay for the given number of milliseconds. +procTests.waitMsec = function (msec) { + var t0 = Date.now(); + var longEnough = false; + while (!longEnough) { + longEnough = ((Date.now() - t0) > msec); + } +}; + +var processesBridge = gpii.processes.windows(); + +jqUnit.module("Processes Bridge for Windows module"); +jqUnit.test( + "Test getProceses('node')/findProcessByPid() with the nodejs process itself", + function () { + var procInfos = processesBridge.getProcessList("node.exe"); + jqUnit.assertNotEquals( + "Listing all processes", 0, procInfos.length + ); + + // Check for the presence of this nodejs processs itself -- it must + // be in the process list since this code is running inside that + // process. + var nodeProc = processesBridge.findProcessByPid(process.pid, procInfos); + jqUnit.assertNotNull("Searching for 'node' process", nodeProc); + } +); + +jqUnit.test( + "Test getProceses()/findProcessByPid() with the nodejs process itself", + function () { + var procInfos = processesBridge.getProcessList(); + jqUnit.assertNotEquals( + "Listing all processes", 0, procInfos.length + ); + + // Check for the presence of this nodejs processs itself -- it must + // be in the process list since this code is running inside that + // process. + var nodeProc = processesBridge.findProcessByPid(process.pid, procInfos); + jqUnit.assertNotNull("Searching for 'node' process", nodeProc); + } +); + +jqUnit.test( + "Test findProcessByPid() with non-running process id", + function () { + jqUnit.assertNull( + "Search negative process id value", processesBridge.findProcessByPid(-1) + ); + } +); + +jqUnit.test( + "Test findProcessByPid() against nodejs's own process object.", + function () { + var nodeProcInfo = processesBridge.findProcessByPid(process.pid); + jqUnit.assertEquals("Node process 'name'", + path.parse(process.execPath).base, nodeProcInfo.command); + + jqUnit.assertEquals("Node process 'pid'", + process.pid, nodeProcInfo.pid); + + jqUnit.assertEquals("Node process 'argv' length'", + process.argv.length + process.execArgv.length, nodeProcInfo.argv.length); + + jqUnit.assertEquals("Node process status", + "Running", nodeProcInfo.state); + + // The "fullPath" property is added by the process node add-on. + // It should match the full path to process.title. + jqUnit.assertEquals("Node process fullPath", + process.execPath, nodeProcInfo.fullPath + ); + + // The order of process.argv and nodeProcInfo.argv is not + // necessarily the same, nor are the number of arguments the same. + // Only the first argument of the vectors match. + debugger; + jqUnit.assertEquals("Node process argv[0]", + path.basename(process.argv[0]), + path.basename(nodeProcInfo.argv[0]) + ".exe" + ); + } +); + +jqUnit.test( + "Test findProcessesByCmd()/findFirstProcessByCmd() with nodejs itself", + function () { + var nodeProcInfos = processesBridge.findProcessesByCommand("node.exe"); + jqUnit.assertNotEquals( + "Getting all 'node' processes", 0, nodeProcInfos.length + ); + nodeProcInfos.forEach(function (aProcInfo) { + jqUnit.assertEquals( + "Node commmand name", "node.exe", aProcInfo.command + ); + }); + var procInfo = processesBridge.findFirstProcessByCommand("node.exe"); + jqUnit.assertNotNull( + "Looking for first 'node' processes", procInfo); + jqUnit.assertEquals("Node commmand name", "node.exe", procInfo.command); + } +); + +jqUnit.test( + "Test initProcInfoNotRunning()", + function () { + var notRunning = processesBridge.initProcInfoNotRunning("grep"); + jqUnit.assertEquals("Command name", notRunning.command, "grep"); + jqUnit.assertEquals("Negative process id", notRunning.pid, -1); + jqUnit.assertEquals( + "'NoSuchProcess' state", notRunning.state, "NoSuchProcess" + ); + jqUnit.assertNull( + "Search negative process id value", + processesBridge.findProcessByPid(notRunning.pid) + ); + } +); + +jqUnit.test( + "Test isRunning() with nodejs itself, and nonexistent process", + function () { + var procInfo = processesBridge.findProcessByPid(process.pid); + jqUnit.assertNotNull("Searching for 'node' process", procInfo); + jqUnit.assertTrue( + "Check nodejs is running", + processesBridge.isRunning(procInfo.state) + ); + procInfo = processesBridge.initProcInfoNotRunning("grep"); + jqUnit.assertFalse( + "Check nonexistent process running", + processesBridge.isRunning(procInfo) + ); + } +); + +jqUnit.test( + "Test updateProcInfo() against non-changing process", + function () { + var procInfo = processesBridge.findProcessByPid(process.pid); + jqUnit.assertNotNull("Looking for 'node' processes", procInfo); + var newProcInfo = processesBridge.updateProcInfo(procInfo); + jqUnit.assertDeepEq( + "Check change in process info", procInfo, newProcInfo + ); + } +); + +jqUnit.test( + "Test updateProcInfo() against changing process", + function () { + var notePad = spawn("notepad.exe"); + var notePadInfo = processesBridge.findProcessByPid(notePad.pid); + jqUnit.assertNotNull("Search 'notepad' process", notePadInfo); + jqUnit.assertTrue("Stop Notepad", notePad.kill("SIGKILL")); + procTests.waitMsec(500); // Allow system cleanup. + var newNotePadInfo = processesBridge.updateProcInfo(notePadInfo); + jqUnit.assertNotEquals( + "Update process state", newNotePadInfo.state, notePadInfo.state + ); + } +); + +jqUnit.test( + "Test findSolutionsByCommands()", + function () { + // Node is running. Add a running notepad process. No such command as why. + var notePad = spawn("notepad.exe"); + var solutions = ["node.exe", "notepad.exe", "why.exe"]; + var procInfos = processesBridge.findSolutionsByCommands(solutions); + jqUnit.assertTrue("Node and Notepad processes", procInfos.length >= 2); + procInfos.forEach(function (item) { + var isNode = item.command === "node.exe"; + var isCat = item.command === "notepad.exe"; + jqUnit.assertTrue("Process name node or notepad", isNode || isCat); + }); + notePad.kill("SIGKILL"); + } +); + +jqUnit.test( + "Test findSolutionsByPids()", + function () { + // Node is running. Add a running notepad process. + var notePad = spawn("notepad"); + var pids = [process.pid, notePad.pid]; + var procInfos = processesBridge.findSolutionsByPids(pids); + jqUnit.assertTrue("Node and Notepad processes", procInfos.length >= 2); + procInfos.forEach(function (item) { + var isNode = item.pid === process.pid; + var isNotepad = item.pid === notePad.pid; + jqUnit.assertTrue("Process pid node or notepad", isNode || isNotepad); + }); + notePad.kill("SIGKILL"); + } +); + diff --git a/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js b/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js index 461bd81f2..30534a8c8 100644 --- a/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js +++ b/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js @@ -62,11 +62,11 @@ var advapi32 = new ffi.Library("advapi32", { RegSetValueExW: [ "long", ["uint32", "pointer", "uint32", "uint32", "pointer", "uint32"] ], - // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724845(v=vs.85).aspx + // https://msdn.microsoft.com/library/ms724847 // HKEY, LPCWSTR // Similar abuse to RegOpenKeyExW - for convenience, only accepts base keys - RegDeleteKeyW: [ - "long", ["uint32", "pointer"] + RegDeleteKeyExW: [ + "long", ["uint32", "pointer", "uint32", "ulong"] ], // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724851(v=vs.85).aspx // HKEY, LPCWSTR @@ -136,10 +136,44 @@ windows.getBaseKey = function (baseKey) { var cr = windows.checkReturnCode; +/** + * Parses a registry key path, allowing to specify if the 32 or 64-bit view should be used instead of the one correct + * for the processes. The view is specified by prefixing the real path with "32:" or "64:" (eg, "64:Software\GPII"). + * Currently, this is only used when retrieving the MachineGuid. + * + * The return object contains the real path, and a value (KEY_WOW64_64KEY, KEY_WOW64_32KEY, or 0) to bitwise "or" with + * the samDesired parameter of RegCreateKeyEx, + * RegOpenKeyEx, or RegDeleteKeyEx. + * + * @param path {String} The path + * @return {Object} Object containing the real path, and access mask. + */ +windows.parseRegistryPath = function (path) { + var togo = { + desiredAccess: 0, + realPath: path + }; + + // To explicitly use either the 32 or 64bit view, prepend the path with "32:" or "64:". + if (path[2] === ":") { + var bits = path.substr(0, 2); + if (bits === "64") { + togo.desiredAccess |= c.KEY_WOW64_64KEY; + } else if (bits === "32") { + togo.desiredAccess |= c.KEY_WOW64_32KEY; + } + + togo.realPath = path.substr(3); + } + + return togo; +}; + windows.openRegistryKey = function (baseKey, path, keyHolder) { - var pathW = windows.ensureAlignment(windows.toWideChar(path).pointer); + var regPath = windows.parseRegistryPath(path); + var pathW = windows.ensureAlignment(windows.toWideChar(regPath.realPath).pointer); var base = windows.getBaseKey(baseKey); - var code = advapi32.RegOpenKeyExW(base, pathW, 0, c.KEY_QUERY_VALUE, keyHolder); + var code = advapi32.RegOpenKeyExW(base, pathW, 0, regPath.desiredAccess | c.KEY_QUERY_VALUE, keyHolder); if (code === windows.API_constants.returnCodes.FILE_NOT_FOUND) { return { statusCode: 404 @@ -198,10 +232,12 @@ windows.writeRegistryKey = function (baseKey, path, subKey, value, type) { var togo = { statusCode: 200 }; - var pathW = windows.ensureAlignment(windows.toWideChar(path).pointer); + var regPath = windows.parseRegistryPath(path); + var pathW = windows.ensureAlignment(windows.toWideChar(regPath.realPath).pointer); var keyHolder = new Buffer(4); var base = windows.getBaseKey(baseKey); - var code = advapi32.RegCreateKeyExW(base, pathW, 0, NULL, 0, c.KEY_SET_VALUE, NULL, keyHolder, NULL); + var code = advapi32.RegCreateKeyExW(base, pathW, 0, NULL, 0, regPath.desiredAccess | c.KEY_SET_VALUE, NULL, + keyHolder, NULL); cr(code); try { @@ -229,9 +265,10 @@ windows.writeRegistryKey = function (baseKey, path, subKey, value, type) { // implemented manually on top of this existing API if required. // NB: This utility is currently used only to clean up state from within test cases windows.deleteRegistryKey = function (baseKey, path) { - var pathW = windows.ensureAlignment(windows.toWideChar(path).pointer); + var regPath = windows.parseRegistryPath(path); + var pathW = windows.ensureAlignment(windows.toWideChar(regPath.realPath).pointer); var base = windows.getBaseKey(baseKey); - var code = advapi32.RegDeleteKeyW(base, pathW); + var code = advapi32.RegDeleteKeyExW(base, pathW, regPath.desiredAccess, 0); cr(code); }; diff --git a/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js b/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js index cdc48562b..1638a306e 100644 --- a/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js +++ b/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js @@ -14,7 +14,8 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("universal"), + os = require("os"); var jqUnit = fluid.require("node-jqunit"); var gpii = fluid.registerNamespace("gpii"); @@ -294,3 +295,53 @@ jqUnit.test("Enumerating registry key values", function () { // Clean up gpii.windows.deleteRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier"); }); + +jqUnit.test("Testing 32/64 bit views", function () { + + // HKEY_LOCAL_MACHINE\Software has different values for 32/64-bit processes, however this one is write-able. + // https://msdn.microsoft.com/library/aa384253 + var baseKey = "HKEY_CURRENT_USER"; + var path = "Software\\Classes\\CLSID\\GPII-Test"; + var path32 = "32:" + path; + var path64 = "64:" + path; + + var testValue = "test"; + var bits = (os.arch() === "x64") ? "64" : "32"; + + // 32-bit read/write + gpii.windows.writeRegistryKey(baseKey, path32, "test32", testValue, "REG_SZ"); + var result1 = gpii.windows.readRegistryKey(baseKey, path32, "test32", "REG_SZ"); + jqUnit.assertEquals("32-bit registry view should work", testValue, result1.value); + + // 64-bit read/write + gpii.windows.writeRegistryKey(baseKey, path64, "test64", testValue, "REG_SZ"); + var result2 = gpii.windows.readRegistryKey(baseKey, path64, "test64", "REG_SZ"); + jqUnit.assertEquals("64-bit registry view should work", testValue, result2.value); + + if (gpii.windows.isWow64() || bits === "64") { + // These tests aren't valid on a 32-bit OS because there is only 1 registry view; 32-bit and 64-bit views + // point to the same thing, so both values will exist. + + // Try 32-bit value from 64-bit + var result3 = gpii.windows.readRegistryKey(baseKey, path64, "test32", "REG_SZ"); + jqUnit.assertUndefined("32-bit value should not exist in 64-bit registry view", result3.value); + + // Try 32-bit value from 64-bit + var result4 = gpii.windows.readRegistryKey(baseKey, path32, "test64", "REG_SZ"); + jqUnit.assertUndefined("64-bit value should not exist in 32-bit registry view", result4.value); + + // Get native value from native view + var result5 = gpii.windows.readRegistryKey(baseKey, path, "test" + bits, "REG_SZ"); + jqUnit.assertEquals("native-bit value should be in native-bit registry view", testValue, result5.value); + + // Try other value from native view + var otherBits = (bits === "64") ? "32" : "64"; + var result6 = gpii.windows.readRegistryKey(baseKey, path, "test" + otherBits, "REG_SZ"); + jqUnit.assertUndefined("non-native-bit value should not be in in native-bit registry view", result6.value); + } + + // Clean up + gpii.windows.deleteRegistryKey(baseKey, path32); + gpii.windows.deleteRegistryKey(baseKey, path64); + +}); diff --git a/gpii/node_modules/windowsMetrics/index.js b/gpii/node_modules/windowsMetrics/index.js new file mode 100644 index 000000000..baddbb717 --- /dev/null +++ b/gpii/node_modules/windowsMetrics/index.js @@ -0,0 +1,25 @@ +/* + * Windows metrics capturing. + * + * 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 fluid = require("infusion"); + +fluid.module.register("windowsMetrics", __dirname, require); + +require("./src/windowsMetrics.js"); diff --git a/gpii/node_modules/windowsMetrics/package.json b/gpii/node_modules/windowsMetrics/package.json new file mode 100644 index 000000000..0c3fd4026 --- /dev/null +++ b/gpii/node_modules/windowsMetrics/package.json @@ -0,0 +1,13 @@ +{ + "name": "windowsMetrics", + "description": "Windows Metrics.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/windows.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} \ No newline at end of file diff --git a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js new file mode 100644 index 000000000..983ddecd0 --- /dev/null +++ b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js @@ -0,0 +1,463 @@ +/* + * 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"); +var fluid = require("infusion"); + +var windows = fluid.registerNamespace("gpii.windows"); + +fluid.require("%gpii-windows/gpii/node_modules/WindowsUtilities/WindowsUtilities.js"); + + +fluid.defaults("gpii.windowsMetrics", { + gradeNames: ["fluid.modelComponent", "fluid.contextAware"], + 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", + "onStartMetrics.application": "{that}.startApplicationMetrics", + "onStopMetrics.application": "{that}.stopApplicationMetrics", + "onStartMetrics.keyboard": "{that}.startKeyboardMetrics", + "onStopMetrics.keyboard": "{that}.stopKeyboardMetrics" + }, + events: { + "onStartMetrics": null, + "onStopMetrics": null + }, + invokers: { + logMetric: { + func: "{eventLog}.logEvent", + args: ["{eventLog}", "metrics", "{arguments}.0", "{arguments}.1"] + }, + startApplicationMetrics: { + funcName: "gpii.windows.startApplicationMetrics", + args: ["{that}"] + }, + stopApplicationMetrics: { + funcName: "gpii.windows.stopApplicationMetrics", + args: ["{that}"] + }, + startKeyboardMetrics: { + funcName: "gpii.windows.startKeyboardMetrics", + args: ["{that}"] + }, + stopKeyboardMetrics: { + funcName: "gpii.windows.stopKeyboardMetrics", + args: ["{that}"] + } + }, + model: { + config: { + application: { + // Milliseconds to poll the active window. + precision: 5000 + }, + keyboard: { + // Number of records per log line (falsey for no limit). + maxRecords: 500, + // Minimum typing session time, in milliseconds. + minSession: 30000, + // The time a session will last with no activity, in milliseconds. + sessionTimeout: 60000 + } + }, + state: { + application: {}, + keyboard: { + keyboardHookHandle: null, + lastKeyLogged: null, + lastKeyTime: 0, + times: [] + } + } + } +}); + +fluid.defaults("gpii.windowsMetrics.windows", { + listeners: { + "{eventLog}.events.onCreate": "{that}.events.onStartMetrics" + } +}); + +// Don't auto-start the metrics capture when testing. +fluid.defaults("gpii.windowsMetrics.test", { +}); + +fluid.defaults("gpii.eventLog.windows", { + components: { + windowsMetrics: { + type: "gpii.windowsMetrics" + } + } +}); + +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; +}; + +/** + * 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 that {Component} The gpii.windowsMetrics instance. + */ +windows.checkNewApplication = function (that) { + var runningApplications = that.model.state.application.runningApplications; + var pid = that.model.state.application.currentProcess.pid; + var exePath = that.model.state.application.currentProcess.exe; + + // pid might have been re-used. + var oldExe = runningApplications[pid]; + var isNew = oldExe !== exePath; + + if (isNew) { + var data = { + exe: exePath + }; + + that.logMetric("app-launch", data); + + if (!oldExe) { + runningApplications.count++; + } + runningApplications[pid] = exePath; + + // It doesn't need to be constantly kept up to date, but trim it at a certain point. + if (runningApplications.count > 30) { + for (var key in runningApplications) { + if (runningApplications.hasOwnProperty(key) && !isNaN(key)) { + if (!windows.isProcessRunning(key)) { + delete runningApplications[key]; + runningApplications.count--; + } + } + } + } + } +}; + +/** + * Begin monitoring the application launches and active windows. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.startApplicationMetrics = function (that) { + that.model.state.application = { + active: true, + runningApplications: { + count: 0 + } + }; + windows.checkActiveWindow(that, null); +}; + +/** + * Stops collecting the application metrics. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.stopApplicationMetrics = function (that) { + that.model.state.application.active = false; + // Log the currently active window as though it was de-activated. + windows.logAppActivate(that); +}; + +/** + * 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 that {Component} The gpii.windowsMetrics instance. + */ +windows.logAppActivate = function (that) { + if (that.model.state.application.currentProcess) { + var duration = process.hrtime(that.model.state.application.currentProcess.timeActivated); + var data = { + exe: that.model.state.application.currentProcess.exe, + // Round to the nearest second. + duration: duration[0] + (duration[1] < 5e8 ? 0 : 1) + }; + that.logMetric("app-active", data); + } +}; +/** + * Begin checking if the active Window has changed. It will continue to check while application.state.active is true. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.checkActiveWindow = function (that) { + var activePid = 0; + var lastHwnd = 0; + if (!that.model.state.application.currentProcess) { + that.model.state.application.currentProcess = {}; + } + that.model.state.application.currentProcess.timeActivated = process.hrtime(); + + windows.waitForCondition(function () { + // Get the process ID that owns the active Window. + var hwnd = windows.user32.GetForegroundWindow(); + if (hwnd) { + if (hwnd !== lastHwnd) { + lastHwnd = hwnd; + } + activePid = windows.getWindowProcessId(hwnd); + } + return activePid !== that.model.state.application.currentProcess.pid || !that.model.state.application.active; + }, { + pollDelay: that.model.config.application.precision + }).then(function () { + if (that.model.state.application.active) { + // Log how long the last window was active. + windows.logAppActivate(that); + + var exePath = windows.getProcessPath(activePid); + that.model.state.application.currentProcess.pid = activePid; + that.model.state.application.currentProcess.exe = exePath; + + // Also perform the "application launch" metric here. A process having its window activated implies it's + // been launched. + windows.checkNewApplication(that); + windows.checkActiveWindow(that); + } + }); +}; + +/** + * Starts the key stroke metrics. + * + * This sets up a low-level keyboard hook (WH_KEYBOARD_LL) that makes the system invoke a call-back whenever a key is + * pressed or released. (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 (otherwise this becomes an interesting way of disabling the keyboard). + * - 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. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.startKeyboardMetrics = function (that) { + windows.stopKeyboardMetrics(that); + if (process.versions.electron) { + // The callback needs to be referenced outside here otherwise the GC will pull the rug from beneath it. + windows.keyboardHookCallback = ffi.Callback( + windows.types.HANDLE, [windows.types.INT, windows.types.HANDLE, windows.KBDLLHookStructPointer], + function (code, wparam, lparam) { + windows.keyboardHook(that, code, wparam, lparam); + }); + + var hModule = windows.kernel32.GetModuleHandleW(0); + that.model.state.keyboard.keyboardHookHandle = + windows.user32.SetWindowsHookExW(13, windows.keyboardHookCallback, hModule, 0); + + if (!that.model.state.keyboard.keyboardHookHandle) { + windows.keyboardHookCallback = null; + fluid.fail("SetWindowsHookExW did not work. win32 error: " + windows.kernel32.GetLastError()); + } + + } else { + // The keyboard hook's ability to work is a side-effect of running with electron. + fluid.log(fluid.logLevel.WARN, "Keyboard metrics not available without Electron."); + } + + that.model.state.keyboard.config = that.model.config.keyboard; +}; + +/** + * Disables the key stroke metrics. + * + * Removes the low-level keyboard hook from the system, and sends the last timings to the log. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.stopKeyboardMetrics = function (that) { + // remove the keyboard hook + if (that.model.state.keyboard.keyboardHookHandle) { + windows.user32.UnhookWindowsHookEx(that.model.state.keyboard.keyboardHookHandle); + } + that.model.state.keyboard.keyboardHookHandle = null; + that.model.state.keyboard.keyboardHookCallback = null; + // flush the log. + if (that.model.state.keyboard.times) { + windows.logKeyTimings(that); + that.model.state.keyboard.times = []; + } +}; + +/** + * Keys that have a special meaning. + */ +windows.specialKeys = fluid.freezeRecursive((function () { + var special = { + "BS": windows.API_constants.VK_BACK, + "DEL": windows.API_constants.VK_DELETE, + "ESC": windows.API_constants.VK_ESCAPE, + "LEFT": windows.API_constants.VK_LEFT + }; + + // Swap to ease look-ups. + fluid.each(special, function (value, key) { + special[value] = key; + }); + + return special; +})()); + +/** + * Sends the key timings to the log. + * + * @param times {Array} Array of key times. + * @param maxRecords {Number} Maximum key times per log entry. + */ +windows.logKeyTimings = function (that) { + var times = that.model.state.keyboard.times; + var maxRecords = that.model.config.keyboard.maxRecords || times.length; + + for (var offset = 0; offset < times.length; offset += maxRecords) { + // Split the list of timings into chunks. + var data = { + times: times.slice(offset, offset + maxRecords) + }; + that.logMetric("key-times", data); + } +}; + +/** + * 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 state {Object} The keyboard metrics state. + * @param timestamp {Number} Milliseconds since a fixed point in time. + * @param specialKey {String} The value key, if it's a special key. + */ +windows.recordKeyTiming = function (that, timestamp, specialKey) { + var state = that.model.state.keyboard; + if (!state.times) { + state.times = []; + } + + // The time since the last key press + var keyTime = state.lastKeyTime ? timestamp - state.lastKeyTime : 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.times.length === 1) && (keyTime > state.config.minSession)) { + // This key was hit after the minimum session time, so ignore the last key. + state.times = []; + } else if ((state.times.length > 1) && (keyTime > state.config.sessionTimeout)) { + // Store the current session, and begin a new one. + windows.logKeyTimings(that); + state.times = []; + keyTime = 0; + } + + state.lastKeyTime = timestamp; + + var record = { + t: keyTime + }; + + if (specialKey) { + // Double-check that only certain keys are being recorded (it would be a serious blunder). + if (windows.specialKeys[specialKey] && typeof (specialKey) === "string" && specialKey.length > 1) { + record.key = windows.specialKeys[windows.specialKeys[specialKey]]; + } + } + + state.times.push(record); +}; + +/** + * The keyboard hook callback. Called when a key is pressed or released. + * https://msdn.microsoft.com/library/ms644985 + * + * @param that {Component} The gpii.windowsMetrics instance. + * @param code {Number} If less than zero, then don't process. + * @param wparam {Number} The keyboard message. One of WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP + * @param lparam {Number} A pointer to a KBDLLHOOKSTRUCT structure. + * @return {Number} The return value of CallNextHookEx. + */ +windows.keyboardHook = function (that, code, wparam, lparam) { + var togo; + try { + if (code >= 0 && wparam === windows.API_constants.WM_KEYUP) { + var wanted = false; + // For testing, allows the object to be passed directly rather than a ffi/ref struct. + var kb = lparam.deref ? lparam.deref() : lparam; + + var specialKey = windows.specialKeys[kb.vkCode]; + + // Ignore injected and Alt keys. + var ignoreFlags = windows.API_constants.LLKHF_INJECTED | windows.API_constants.LLKHF_ALTDOWN; + + if ((kb.flags & ignoreFlags) === 0) { + // If the key doesn't generate a character, then don't count it. + wanted = specialKey || windows.user32.MapVirtualKeyW(kb.vkCode, windows.API_constants.MAPVK_VK_TO_CHAR); + if (kb.vkCode === windows.API_constants.VK_LEFT) { + // Special case for the 'left' cursor key - if the last key was logged then log this key as it was + // probably used for correcting a mistake. + wanted = that.model.state.keyboard.lastKeyLogged; + } + if (wanted) { + // Process in the next tick, to allow this function to return soon. + process.nextTick(windows.recordKeyTiming, that, kb.time, specialKey); + } + + that.model.state.keyboard.lastKeyLogged = wanted && !specialKey; + } + } + } finally { + // This needs to be called, or the key will be lost. + togo = windows.user32.CallNextHookEx(0, code, wparam, lparam); + } + return togo; +}; diff --git a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js new file mode 100644 index 000000000..8f143c787 --- /dev/null +++ b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js @@ -0,0 +1,875 @@ +/* + * eventLog Tests + * + * 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 fluid = require("universal"), + fs = require("fs"), + os = require("os"), + path = require("path"), + readline = require("readline"), + child_process = require("child_process"); + +require("../../processHandling/processHandling.js"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.metrics"); + +require("../index.js"); + +var teardowns = []; + +jqUnit.module("gpii.tests.metrics", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +fluid.logObjectRenderChars = 0xffff; + +// Wrap the gpii.windowsMetrics to disable auto-starting the metrics capturing. +fluid.defaults("gpii.tests.metrics.windowsMetricsWrapper", { + gradeNames: ["fluid.component", "gpii.windowsMetrics", "gpii.eventLog", "gpii.lifecycleManager"], + listeners: { + "onStartMetrics.application": null, + "onStartMetrics.keyboard": null, + "onStopMetrics.application": null, + "onStopMetrics.keyboard": null + } +}); + +// Tests logKeyTimings, splitting of key timings into separate log entries. +gpii.tests.metrics.logKeyTimingsTests = fluid.freezeRecursive([ + { // Single record + input: { + times: [{t: 10}], + maxRecords: 1 + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 10} ] } + } + }, + { // Multiple records, 1 line each + input: { + times: [{t: 20}, {t: 21}, {t: 22}], + maxRecords: 1 + }, + expect: [ + { + module: "metrics", + event: "key-times", + data: { times: [{t: 20}] } + }, + { + module: "metrics", + event: "key-times", + data: { times: [{t: 21}] } + }, + { + module: "metrics", + event: "key-times", + data: { times: [{t: 22}] } + } + ] + }, + { // 2 sets of 3 records + input: { + times: [{t: 30}, {t: 31}, {t: 32}, {t: 33}, {t: 34}, {t: 35}], + maxRecords: 3 + }, + expect: [ + { + module: "metrics", + event: "key-times", + data: { + times: [{t: 30}, {t: 31}, {t: 32}] + } + }, + { + module: "metrics", + event: "key-times", + data: { + times: [{t: 33}, {t: 34}, {t: 35}] + } + } + ] + }, + { + // Imperfect number + input: { + times: [{t: 40}, {t: 41}, {t: 42}, {t: 43}, {t: 44}], + maxRecords: 3 + }, + expect: [ + { + module: "metrics", + event: "key-times", + data: { + times: [{t: 40}, {t: 41}, {t: 42}] + } + }, + { + module: "metrics", + event: "key-times", + data: { + times: [{t: 43}, {t: 44}] + } + } + ] + }, + { + // No cut-off + input: { + times: [{t: 50}, {t: 51}, {t: 52}, {t: 53}, {t: 54}, {t: 55}, {t: 56}, {t: 57}, {t: 58}, {t: 59}], + maxRecords: null + }, + expect: [ + { + module: "metrics", + event: "key-times", + data: { + times: [{t: 50}, {t: 51}, {t: 52}, {t: 53}, {t: 54}, {t: 55}, {t: 56}, {t: 57}, {t: 58}, {t: 59}] + } + } + ] + } +]); + +gpii.tests.metrics.defaultKeyboardConfig = fluid.freezeRecursive(); + +// Tests for recordKeyTiming: key presses +gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ + defaultState: { + times: [] + }, + defaultConfig: { + minSession: 0xffffff, + sessionTimeout: 0xffffff, + maxRecords: 1000 + }, + testData: [ + { // First key press + state: {}, + input: { + timestamp: 1, + key: undefined + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0}]} + } + }, + { // Key press, existing state + state: null, + input: { + timestamp: 1, + key: undefined + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0}]} + } + }, + { // Key press, existing state with previous keys + state: { + times: [{t:10}, {t:11}, {t:12} ] + }, + input: { + timestamp: 1, + key: undefined + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 10}, {t: 11}, {t: 12}, {t: 0} ]} + } + }, + { // Key press, existing state with previous keys and lastKeyTime set + state: { + times: [{t:20}, {t:21}, {t:22}], + lastKeyTime: 100 + }, + input: { + timestamp: 200, + key: undefined + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 20}, {t: 21}, {t: 22}, {t: 100} ]} + } + }, + { // special key + state: null, + input: { + key: "BS" + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: "BS"}]} + } + }, + // Test that non-special keys don't get leaked. + { // not a special key (letter) + state: null, + input: { + key: "A" + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: fluid.NO_VALUE }]} + } + }, + { // not a special key (symbol) + state: null, + input: { + key: "!" + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: fluid.NO_VALUE }]} + } + }, + { // not a special key (numeric string) + state: null, + input: { + key: "1" + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: fluid.NO_VALUE }]} + } + }, + { // not a special key (number) + state: null, + input: { + key: 0x41 + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: fluid.NO_VALUE }]} + } + }, + { // not a special key (string) + state: null, + input: { + key: "ABC" + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: fluid.NO_VALUE }]} + } + }, + { // not a special key (virtual key code of a special key) + state: null, + input: { + key: gpii.windows.API_constants.VK_ESCAPE + }, + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0, key: fluid.NO_VALUE }]} + } + } + ] +}); + +// Tests for recordKeyTiming: typing sessions +gpii.tests.metrics.typingSessionTests = fluid.freezeRecursive({ + defaultState: { + times: [] + }, + defaultConfig: { + minSession: 30, + sessionTimeout: 60, + maxRecords: 1000 + }, + testData: [ + { // Single session + state: { + times: [] + }, + input: [ + { timestamp: 1 }, + { timestamp: 2 }, + { timestamp: 4 }, + { timestamp: 8 } + ], + expect: { + module: "metrics", + event: "key-times", + data: {times: [{t: 0}, {t: 1}, {t: 2}, {t: 4} ]} + } + }, + { // Two sessions + state: { + times: [] + }, + input: [ + {timestamp: 1}, + {timestamp: 2}, + {timestamp: 3}, + {timestamp: 4}, + {timestamp: 200}, + {timestamp: 202}, + {timestamp: 204}, + {timestamp: 206} + ], + expect: [{ + module: "metrics", + event: "key-times", + data: {times: [{t: 0}, {t: 1}, {t: 1}, {t: 1}]} + }, { + module: "metrics", + event: "key-times", + data: {times: [{t: 0}, {t: 2}, {t: 2}, {t: 2}]} + }] + } + ] +}); + +// Tests for keyboardHook. +gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ + { // Invalid nCode. + input: { + nCode: -1, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: "A".charCodeAt(), + scanCode: 0, + flags: 0, + time: 1, + dwExtraInfo: 0 + } + }, + expect: [] + }, + { // First key. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: "A".charCodeAt(), + scanCode: 0, + flags: 0, + time: 150, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 0, key: fluid.NO_VALUE} ] } + } + }, + { // Next key (50ms later). + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: "A".charCodeAt(), + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 50, key: fluid.NO_VALUE} ] } + } + }, + { // Non-special non-input key. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: gpii.windows.API_constants.VK_HOME, + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: [] + }, + { // Special key: back space. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: gpii.windows.API_constants.VK_BACK, + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 0, key: "BS"} ] } + } + }, + { // Special key: delete. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: gpii.windows.API_constants.VK_DELETE, + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 0, key: "DEL"} ] } + } + }, + { // Special key: escape. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: gpii.windows.API_constants.VK_ESCAPE, + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 0, key: "ESC"} ] } + } + }, + { // Special key: left arrow. Expect nothing, because this only logs if hit after a character key. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: gpii.windows.API_constants.VK_LEFT, + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: [] + }, + { // Normal key: 'X' + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: "X".charCodeAt(), + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 0, key: fluid.NO_VALUE } ] } + } + }, + { // Special key: left arrow. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_KEYUP, + lParam: { + vkCode: gpii.windows.API_constants.VK_LEFT, + scanCode: 0, + flags: 0, + time: 200, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "key-times", + data: { times: [ {t: 0, key: "LEFT"} ] } + } + } +]); + +/** + * Configure a log file that's just used for testing. + * The file will be deleted automatically after the current test completes. + * + * @return {String} The path of the log file. + */ +gpii.tests.metrics.setLogFile = function () { + var logFile = os.tmpdir() + "/gpii-test-eventLog-" + Date.now(); + teardowns.push(function () { + fs.unlinkSync(logFile); + }); + gpii.eventLog.logFilePath = logFile; + return logFile; +}; + +/** + * 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.metrics.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.isMarker(exp, fluid.VALUE)) { + // match any value + match = subject.hasOwnProperty(prop); + } else if (fluid.isMarker(exp, fluid.NO_VALUE)) { + // match no value + match = !subject.hasOwnProperty(prop); + } else if (fluid.isPrimitive(exp)) { + match = subject[prop] === exp; + } else { + match = gpii.tests.metrics.deepMatch(subject[prop], exp, maxDepth - 1); + } + if (!match) { + break; + } + } + } + + return match; +}; + +/** + * Checks the log file for some lines that are expected. (see deepMatch for the matching rules) + * + * @param logFile {String} The file to read. + * @param expected {Object[]} An array of log items to search for (in order of how they should be found). + * @returns {Promise} Resolves when all expected lines where found, or rejects if the end of the log is reached first. + */ +gpii.tests.metrics.expectLogLines = function (logFile, expected) { + jqUnit.expect(expected.length); + var promise = fluid.promise(); + var reader = readline.createInterface({ + input: fs.createReadStream(logFile) + }); + + var remainingLines = []; + + var index = 0; + var currentExpected = expected[index]; + var complete = false; + + reader.input.on("error", function (err) { + jqUnit.fail(err); + }); + + reader.on("line", function (line) { + if (complete) { + return; + } + + var obj = JSON.parse(line); + remainingLines.push(obj); + + if (gpii.tests.metrics.deepMatch(obj, currentExpected)) { + jqUnit.assert("Found a matching line"); + index++; + remainingLines = []; + if (index >= expected.length) { + complete = true; + reader.close(); + } else { + currentExpected = expected[index]; + } + } + }); + + reader.on("close", function () { + if (complete) { + promise.resolve(); + } else { + fluid.log("No matching line (index=" + index + "):", currentExpected); + fluid.log("Remaining log lines:", remainingLines); + promise.reject("Unable to find matching log entry " + index); + } + }); + + return promise; +}; + +/** + * Completes a test of logging, by checking if the given log file contains the expected lines. + * + * @param logFile {String} The log file. + * @param expectedLines {Object[]} An array of log items to search for (in order of how they should be found). + */ +gpii.tests.metrics.completeLogTest = function (logFile, expectedLines) { + gpii.tests.metrics.expectLogLines(logFile, expectedLines).then(function () { + jqUnit.assert("All expected lines should be found."); + jqUnit.start(); + }, function (err) { + jqUnit.fail(err); + }); +}; +/** + * Test the application metrics - app launches and window times. + */ +jqUnit.asyncTest("Testing application metrics", function () { + jqUnit.expect(3); + + var logFile = gpii.tests.metrics.setLogFile(); + + var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({ + members: { + logFilePath: logFile + } + }); + + windowsMetrics.model.config.application.precision = 100; + + // Start monitoring. + windowsMetrics.startApplicationMetrics(); + + // Start notepad.exe, in order to set the active window to something predictable. + var exePath = path.join(process.env.SystemRoot, "System32\\notepad.exe"); + var child = child_process.spawn(exePath); + // Get the "real" path the process is running as - on 32-bit node "System32" is really "SysWOW32" + var runningPath = gpii.windows.getProcessPath(child.pid); + + // Get the currently active window. The only time there isn't an active window is when the focus is changing. + var originalWindow = gpii.windows.user32.GetForegroundWindow(); + jqUnit.assertTrue("The should already be an active window", !!originalWindow); + var originalExe = gpii.windows.getProcessPath(gpii.windows.getWindowProcessId(originalWindow)); + + gpii.windows.waitForCondition(function () { + // Wait for the new window to become active. + var currentWindow = gpii.windows.user32.GetForegroundWindow(); + return originalWindow !== currentWindow; + }).then(function () { + // Window should now be active. Kill it after giving the metrics routine a chance to notice it. + jqUnit.assert("test application should have been focused."); + setTimeout(function () { + windowsMetrics.stopApplicationMetrics(); + child.kill(); + }, 500); + }, function () { + jqUnit.fail("Timed out waiting for test window to focus."); + child.kill(); + }); + + var expectedLines = [ + // The window that was already active. + { + module: "metrics", + event: "app-launch", + data: { + exe: originalExe + } + }, + { + module: "metrics", + event: "app-active", + data: { + exe: originalExe, + duration: fluid.VALUE + } + }, + // The test window. + { + module: "metrics", + event: "app-launch", + data: { + exe: runningPath + } + }, + { + module: "metrics", + event: "app-active", + data: { + exe: runningPath, + duration: fluid.VALUE + } + } + ]; + + child.on("close", function () { + gpii.tests.metrics.completeLogTest(logFile, expectedLines); + }); +}); + +jqUnit.asyncTest("Testing keyboard metrics: logKeyTimings", function () { + jqUnit.expect(1); + + var logFile = gpii.tests.metrics.setLogFile(); + var testData = gpii.tests.metrics.logKeyTimingsTests; + var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({ + members: { + logFilePath: logFile + } + }); + + var expectedLines = []; + + fluid.each(testData, function (test) { + windowsMetrics.model.state.keyboard.times = test.input.times; + windowsMetrics.model.config.keyboard.maxRecords = test.input.maxRecords; + gpii.windows.logKeyTimings(windowsMetrics); + expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect)); + }); + + gpii.tests.metrics.completeLogTest(logFile, expectedLines); +}); + +/** + * Test recordKeyTiming + * @param theTests + */ +gpii.tests.metrics.recordKeyTimingTests = function (theTests) { + jqUnit.expect(1); + var logFile = gpii.tests.metrics.setLogFile(); + + var testData = theTests.testData; + var defaultState = theTests.defaultState; + var defaultConfig = theTests.defaultConfig; + + var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({ + members: { + logFilePath: logFile + } + }); + + var expectedLines = []; + + fluid.each(testData, function (test) { + var state = fluid.copy(test.state || defaultState); + state.config = fluid.copy(state.config || defaultConfig); + windowsMetrics.model.state.keyboard = state; + windowsMetrics.model.config.keyboard = state.config; + + // "press" the keys + fluid.each(fluid.makeArray(test.input), function (input) { + gpii.windows.recordKeyTiming(windowsMetrics, input.timestamp, input.key); + }); + + // flush the log + gpii.windows.logKeyTimings(windowsMetrics); + + expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect)); + }); + + gpii.tests.metrics.completeLogTest(logFile, expectedLines); +}; + +jqUnit.asyncTest("Testing keyboard metrics: recordKeyTiming (key presses)", function () { + gpii.tests.metrics.recordKeyTimingTests(gpii.tests.metrics.recordKeyTimingKeyTests); +}); + +jqUnit.asyncTest("Testing keyboard metrics: recordKeyTiming (typing sessions)", function () { + gpii.tests.metrics.recordKeyTimingTests(gpii.tests.metrics.typingSessionTests); +}); + +jqUnit.asyncTest("Testing keyboard metrics: keyboardHook", function () { + jqUnit.expect(1); + + var logFile = gpii.tests.metrics.setLogFile(); + + var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({ + members: { + logFilePath: logFile + } + }); + + // Disable typing sessions + windowsMetrics.model.config.keyboard.minSession = windowsMetrics.model.config.keyboard.sessionTimeout = -1 >>> 0; + windowsMetrics.model.config.keyboard.maxRecords = 1; + windowsMetrics.startKeyboardMetrics(); + + var testData = gpii.tests.metrics.keyboardHookTests; + + var expectedLines = []; + var currentTest = null; + var nextHookReturn = 0; + + // Mock CallNextHookEx - this must be called for every call to the hook handler with the same parameters. + var origCallNextHookEx = gpii.windows.user32.CallNextHookEx; + gpii.windows.user32.CallNextHookEx = function (hhk, nCode, wParam, lParam) { + // hhk is ignored by windows, but check it's 0 because that's the code's intended value. + jqUnit.assertEquals("CallNextHookEx should be called with the correct hhk", 0, hhk); + jqUnit.assertEquals("CallNextHookEx must be called with the correct nCode", currentTest.nCode, nCode); + jqUnit.assertEquals("CallNextHookEx must be called with the correct wParam", currentTest.wParam, wParam); + jqUnit.assertDeepEq("CallNextHookEx must be called with the correct lParam", currentTest.lParam, lParam); + return ++nextHookReturn; + }; + teardowns.push(function () { + gpii.windows.user32.CallNextHookEx = origCallNextHookEx; + }); + + // expect the CallNextHookEx calls (1 assert for each param + the return). + jqUnit.expect(testData.length * 5); + + fluid.each(testData, function (test) { + currentTest = test.input; + var ret = gpii.windows.keyboardHook.call( + null, windowsMetrics, currentTest.nCode, currentTest.wParam, currentTest.lParam); + + jqUnit.assertEquals("keyboardHook must return the result of CallNextHookEx", nextHookReturn, ret); + + expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect)); + }); + + // The keys are put in the log in the next tick (to allow the hook handler to return quickly). + setImmediate(function () { + windowsMetrics.stopKeyboardMetrics(); + gpii.tests.metrics.completeLogTest(logFile, expectedLines); + }); +}); + diff --git a/index.js b/index.js index fca26cf26..302bf28bf 100644 --- a/index.js +++ b/index.js @@ -32,6 +32,8 @@ require("./gpii/node_modules/registrySettingsHandler"); require("./gpii/node_modules/registryResolver"); require("./gpii/node_modules/spiSettingsHandler"); require("./gpii/node_modules/registeredAT/registeredAT.js"); +require("./gpii/node_modules/windowsMetrics"); +require("./gpii/node_modules/processReporter"); require("./gpii/node_modules/platformReporter"); module.exports = fluid; diff --git a/package.json b/package.json index fa208b615..542c57d4a 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "homepage": "http://gpii.net/", "dependencies": { "ffi": "2.0.0", - "ref": "1", - "ref-struct": "1", + "ref": "1.3.4", + "ref-struct": "1.1.0", "ref-array": "1.1.2", + "edge": "6.5.1", "universal": "klown/universal#GPII-1939" }, "devDependencies": { diff --git a/tests/UnitTests.js b/tests/UnitTests.js index 718dd3298..7d77598e8 100644 --- a/tests/UnitTests.js +++ b/tests/UnitTests.js @@ -20,4 +20,6 @@ require("../gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js" require("../gpii/node_modules/processHandling/test/testProcessHandling"); require("../gpii/node_modules/registryResolver/test/testRegistryResolver.js"); require("../gpii/node_modules/registeredAT/test/testRegisteredAT.js"); +require("../gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js"); require("../gpii/node_modules/platformReporter/test/PlatformReporterTests.js"); +require("../gpii/node_modules/processReporter/test/all-tests.js"); From 07534986adec3a4912aa95f16b34e98c1343dda9 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Mon, 11 Sep 2017 13:29:21 -0400 Subject: [PATCH 13/21] GPII-1939: Simplified context aware features --- gpii/node_modules/platformReporter/PlatformReporter.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gpii/node_modules/platformReporter/PlatformReporter.js b/gpii/node_modules/platformReporter/PlatformReporter.js index 78a0240a1..6245d2d58 100644 --- a/gpii/node_modules/platformReporter/PlatformReporter.js +++ b/gpii/node_modules/platformReporter/PlatformReporter.js @@ -25,10 +25,14 @@ fluid.require("%universal"); fluid.defaults("gpii.platformReporter.windows", { gradeNames: ["gpii.platformReporter"], invokers: { - getOSspecifics: "gpii.platformReporter.windows.getOSspecifics" + reportPlatform: { + funcName: "gpii.platformReporter.windows.reportAll", + args: ["{that}"] + } } }); -gpii.platformReporter.windows.getOSspecifics = function () { - return gpii.windows.display.getAllScreenResolutions(); +gpii.platformReporter.windows.reportAll = function (that) { + var allInfo = that.getBasicOS(); + return Object.assign(allInfo, gpii.windows.display.getAllScreenResolutions()); }; From 6daea7d2feaefa077d8bb5ad7f0315f2e5afbb3d Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 15 Sep 2017 16:43:28 -0400 Subject: [PATCH 14/21] GPII-1939 Merge upstream master and branch GPII-2404 into GPII-1939_GPII-2404 --- .../processReporter/dotNetProcesses.csx | 18 ++- .../test/processesBridge_tests.js | 22 +-- .../windowsMetrics/src/windowsMetrics.js | 6 +- .../test/WindowsMetricsTests.js | 139 +++++++++--------- tests/UnitTests.js | 2 +- 5 files changed, 99 insertions(+), 88 deletions(-) diff --git a/gpii/node_modules/processReporter/dotNetProcesses.csx b/gpii/node_modules/processReporter/dotNetProcesses.csx index 7bc9dc146..12cb5a8e7 100644 --- a/gpii/node_modules/processReporter/dotNetProcesses.csx +++ b/gpii/node_modules/processReporter/dotNetProcesses.csx @@ -38,7 +38,7 @@ public class Startup ProcInfo aProcInfo; ArrayList result; - ManagementObjectCollection moc = new ManagementClass(new ManagementPath ("Win32_Process")).GetInstances(); + ManagementObjectCollection moc = new ManagementClass(new ManagementPath("Win32_Process")).GetInstances(); processes = new ManagementObject[moc.Count]; moc.CopyTo(processes, 0); result = new ArrayList(); @@ -82,9 +82,10 @@ public class Startup procInfo.fullPath = propValue.ToString(); } propValue = process.GetPropertyValue("CommandLine"); - if (propValue != null) { - procInfo.argv = makeArgv(propValue.ToString()); + if (propValue == null) { + propValue = ""; } + procInfo.argv = makeArgv(propValue.ToString(), procInfo.fullPath); // Should be able to get the state of the process using the // "ExecutionState" property, but that is not implemented; see: // http://maestriatech.com/wmi.php @@ -113,7 +114,7 @@ public class Startup return procInfo; } - public static String[] makeArgv(String commandLine) + public static String[] makeArgv(String commandLine, String fullPath) { List arguments = new List(); @@ -138,6 +139,15 @@ public class Startup c = endStringIndex; } } + + // In some cases the first argument is just the command, e.g., "Notepad". + // In others, it is the full path and may include the ".exe" + // extension. Normalize it to always be the full path. + if (arguments.Count == 0) { + arguments.Add(fullPath); + } else { + arguments[0] = fullPath; + } return arguments.ToArray(); } } diff --git a/gpii/node_modules/processReporter/test/processesBridge_tests.js b/gpii/node_modules/processReporter/test/processesBridge_tests.js index dd1d30d30..a06ce1dec 100644 --- a/gpii/node_modules/processReporter/test/processesBridge_tests.js +++ b/gpii/node_modules/processReporter/test/processesBridge_tests.js @@ -37,9 +37,10 @@ var processesBridge = gpii.processes.windows(); jqUnit.module("Processes Bridge for Windows module"); jqUnit.test( - "Test getProceses('node')/findProcessByPid() with the nodejs process itself", + "Test getProceses('')/findProcessByPid() using the 'process' object itself", function () { - var procInfos = processesBridge.getProcessList("node.exe"); + var processCommand = path.parse(process.execPath).base; + var procInfos = processesBridge.getProcessList(processCommand); jqUnit.assertNotEquals( "Listing all processes", 0, procInfos.length ); @@ -53,7 +54,7 @@ jqUnit.test( ); jqUnit.test( - "Test getProceses()/findProcessByPid() with the nodejs process itself", + "Test getProceses()/findProcessByPid() with the 'process' object itself", function () { var procInfos = processesBridge.getProcessList(); jqUnit.assertNotEquals( @@ -78,7 +79,7 @@ jqUnit.test( ); jqUnit.test( - "Test findProcessByPid() against nodejs's own process object.", + "Test findProcessByPid() against the 'process' object.", function () { var nodeProcInfo = processesBridge.findProcessByPid(process.pid); jqUnit.assertEquals("Node process 'name'", @@ -94,7 +95,7 @@ jqUnit.test( "Running", nodeProcInfo.state); // The "fullPath" property is added by the process node add-on. - // It should match the full path to process.title. + // It should match the full path to process.execPath. jqUnit.assertEquals("Node process fullPath", process.execPath, nodeProcInfo.fullPath ); @@ -102,10 +103,9 @@ jqUnit.test( // The order of process.argv and nodeProcInfo.argv is not // necessarily the same, nor are the number of arguments the same. // Only the first argument of the vectors match. - debugger; jqUnit.assertEquals("Node process argv[0]", path.basename(process.argv[0]), - path.basename(nodeProcInfo.argv[0]) + ".exe" + path.basename(nodeProcInfo.argv[0]) ); } ); @@ -146,7 +146,7 @@ jqUnit.test( ); jqUnit.test( - "Test isRunning() with nodejs itself, and nonexistent process", + "Test isRunning() with 'process' object itself, and nonexistent process", function () { var procInfo = processesBridge.findProcessByPid(process.pid); jqUnit.assertNotNull("Searching for 'node' process", procInfo); @@ -166,7 +166,7 @@ jqUnit.test( "Test updateProcInfo() against non-changing process", function () { var procInfo = processesBridge.findProcessByPid(process.pid); - jqUnit.assertNotNull("Looking for 'node' processes", procInfo); + jqUnit.assertNotNull("Looking for process.pid processes", procInfo); var newProcInfo = processesBridge.updateProcInfo(procInfo); jqUnit.assertDeepEq( "Check change in process info", procInfo, newProcInfo @@ -199,8 +199,8 @@ jqUnit.test( jqUnit.assertTrue("Node and Notepad processes", procInfos.length >= 2); procInfos.forEach(function (item) { var isNode = item.command === "node.exe"; - var isCat = item.command === "notepad.exe"; - jqUnit.assertTrue("Process name node or notepad", isNode || isCat); + var isNotePad = item.command === "notepad.exe"; + jqUnit.assertTrue("Process name node or notepad", isNode || isNotePad); }); notePad.kill("SIGKILL"); } diff --git a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js index 983ddecd0..8a46c8c70 100644 --- a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js +++ b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js @@ -44,6 +44,10 @@ fluid.defaults("gpii.windowsMetrics", { } }, listeners: { + "{lifecycleManager}.events.onSessionStop": { + "func": "{that}.events.onStopMetrics", + "priority": "before:eventLog" + }, "onDestroy.stopMetrics": "{that}.events.onStopMetrics", "onStartMetrics.application": "{that}.startApplicationMetrics", "onStopMetrics.application": "{that}.stopApplicationMetrics", @@ -105,7 +109,7 @@ fluid.defaults("gpii.windowsMetrics", { fluid.defaults("gpii.windowsMetrics.windows", { listeners: { - "{eventLog}.events.onCreate": "{that}.events.onStartMetrics" + "{lifecycleManager}.events.onSessionStart": "{that}.events.onStartMetrics" } }); diff --git a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js index 8f143c787..b199777de 100644 --- a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js +++ b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js @@ -21,9 +21,7 @@ var fluid = require("universal"), fs = require("fs"), os = require("os"), - path = require("path"), - readline = require("readline"), - child_process = require("child_process"); + readline = require("readline"); require("../../processHandling/processHandling.js"); @@ -43,7 +41,7 @@ jqUnit.module("gpii.tests.metrics", { } }); -fluid.logObjectRenderChars = 0xffff; +//fluid.logObjectRenderChars = 0xffff; // Wrap the gpii.windowsMetrics to disable auto-starting the metrics capturing. fluid.defaults("gpii.tests.metrics.windowsMetricsWrapper", { @@ -535,8 +533,10 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ * @return {String} The path of the log file. */ gpii.tests.metrics.setLogFile = function () { - var logFile = os.tmpdir() + "/gpii-test-eventLog-" + Date.now(); + var previousLogFile = gpii.eventLog.logFilePath; + var logFile = os.tmpdir() + "/gpii-test-metrics-" + Date.now(); teardowns.push(function () { + gpii.eventLog.logFilePath = previousLogFile; fs.unlinkSync(logFile); }); gpii.eventLog.logFilePath = logFile; @@ -634,6 +634,7 @@ gpii.tests.metrics.expectLogLines = function (logFile, expected) { }); reader.on("close", function () { + reader.input.close(); if (complete) { promise.resolve(); } else { @@ -664,7 +665,7 @@ gpii.tests.metrics.completeLogTest = function (logFile, expectedLines) { * Test the application metrics - app launches and window times. */ jqUnit.asyncTest("Testing application metrics", function () { - jqUnit.expect(3); + jqUnit.expect(1); var logFile = gpii.tests.metrics.setLogFile(); @@ -674,76 +675,73 @@ jqUnit.asyncTest("Testing application metrics", function () { } }); - windowsMetrics.model.config.application.precision = 100; - - // Start monitoring. - windowsMetrics.startApplicationMetrics(); + windowsMetrics.model.config.application.precision = 1; + + // Pick three windows (owned by different processes) that already exist, and pretend they've became active by + // mocking GetForegroundWindow. + var windows = []; + var exes = {}; + gpii.windows.enumerateWindows(function (hwnd) { + if (windows.length < 3) { + var exe = gpii.windows.getProcessPath(gpii.windows.getWindowProcessId(hwnd)); + if (!exes[exe]) { + exes[exe] = true; + windows.push({ + exe: exe, + hwnd: hwnd + }); + } + } + }); - // Start notepad.exe, in order to set the active window to something predictable. - var exePath = path.join(process.env.SystemRoot, "System32\\notepad.exe"); - var child = child_process.spawn(exePath); - // Get the "real" path the process is running as - on 32-bit node "System32" is really "SysWOW32" - var runningPath = gpii.windows.getProcessPath(child.pid); - - // Get the currently active window. The only time there isn't an active window is when the focus is changing. - var originalWindow = gpii.windows.user32.GetForegroundWindow(); - jqUnit.assertTrue("The should already be an active window", !!originalWindow); - var originalExe = gpii.windows.getProcessPath(gpii.windows.getWindowProcessId(originalWindow)); - - gpii.windows.waitForCondition(function () { - // Wait for the new window to become active. - var currentWindow = gpii.windows.user32.GetForegroundWindow(); - return originalWindow !== currentWindow; - }).then(function () { - // Window should now be active. Kill it after giving the metrics routine a chance to notice it. - jqUnit.assert("test application should have been focused."); - setTimeout(function () { - windowsMetrics.stopApplicationMetrics(); - child.kill(); - }, 500); - }, function () { - jqUnit.fail("Timed out waiting for test window to focus."); - child.kill(); + // Mock GetForegroundWindow + var activeWindowIndex = 0; + var oldGetForegroundWindow = gpii.windows.user32.GetForegroundWindow; + gpii.windows.user32.GetForegroundWindow = function () { + return windows[activeWindowIndex].hwnd; + }; + teardowns.push(function () { + gpii.windows.user32.GetForegroundWindow = oldGetForegroundWindow; }); - var expectedLines = [ - // The window that was already active. - { - module: "metrics", - event: "app-launch", - data: { - exe: originalExe - } - }, - { - module: "metrics", - event: "app-active", - data: { - exe: originalExe, - duration: fluid.VALUE - } - }, - // The test window. - { - module: "metrics", - event: "app-launch", - data: { - exe: runningPath - } - }, - { - module: "metrics", - event: "app-active", - data: { - exe: runningPath, - duration: fluid.VALUE + var expectedLines = []; + + // Activate the windows a few times. + var activateWindow = function (count) { + activeWindowIndex = (activeWindowIndex + 1) % windows.length; + + if (count > 0) { + + if (!windows[activeWindowIndex].done) { + expectedLines.push({ + module: "metrics", + event: "app-launch", + data: { + exe: windows[activeWindowIndex].exe + } + }); + windows[activeWindowIndex].done = true; } + expectedLines.push({ + module: "metrics", + event: "app-active", + data: { + exe: windows[activeWindowIndex].exe, + duration: fluid.VALUE + } + }); + + setTimeout(activateWindow, 200, count - 1); + } else { + windowsMetrics.stopApplicationMetrics(); + gpii.tests.metrics.completeLogTest(logFile, expectedLines); } - ]; + }; - child.on("close", function () { - gpii.tests.metrics.completeLogTest(logFile, expectedLines); - }); + activateWindow(windows.length * 2); + + // Start monitoring. + windowsMetrics.startApplicationMetrics(); }); jqUnit.asyncTest("Testing keyboard metrics: logKeyTimings", function () { @@ -872,4 +870,3 @@ jqUnit.asyncTest("Testing keyboard metrics: keyboardHook", function () { gpii.tests.metrics.completeLogTest(logFile, expectedLines); }); }); - diff --git a/tests/UnitTests.js b/tests/UnitTests.js index 7d77598e8..cfba37a85 100644 --- a/tests/UnitTests.js +++ b/tests/UnitTests.js @@ -21,5 +21,5 @@ require("../gpii/node_modules/processHandling/test/testProcessHandling"); require("../gpii/node_modules/registryResolver/test/testRegistryResolver.js"); require("../gpii/node_modules/registeredAT/test/testRegisteredAT.js"); require("../gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js"); -require("../gpii/node_modules/platformReporter/test/PlatformReporterTests.js"); require("../gpii/node_modules/processReporter/test/all-tests.js"); +require("../gpii/node_modules/platformReporter/test/PlatformReporterTests.js"); From c9046f346f9f5ba6142594205a1a11030136d69b Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Mon, 18 Sep 2017 10:40:34 -0400 Subject: [PATCH 15/21] GPII-2404: Fixed unit test. Modified "Testing setScreenDpi" to use the "windowsDisplay" component when setting the screen resolution. --- .../test/testDisplaySettingsHandler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index d0418ec56..26b5bc132 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -276,13 +276,14 @@ jqUnit.test("Testing getScreenDpi ", function () { // This test is only able to test the implementation for the version of Windows it's running on. jqUnit.test("Testing setScreenDpi ", function () { // Make sure the resolution is good enough to have DPI above 100%. 1024x768 provides up to 125%. + debugger; var res = gpii.windows.display.getScreenResolution(); if (res.width < 1024 || res.height < 768) { teardowns.push(function () { - gpii.windows.display.setScreenResolution(res); + windowsDisplay.setScreenResolution(res); }); - gpii.windows.display.setScreenResolution({width: 1024, height: 768}); + windowsDisplay({width: 1024, height: 768}); } // Restore the original setting at the end. From 624605a9068101bf020bb95ec10575f7e18dd045 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Mon, 18 Sep 2017 11:28:28 -0400 Subject: [PATCH 16/21] GPII-2404: Fixed typo in unit test. --- .../displaySettingsHandler/test/testDisplaySettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index 26b5bc132..bb7d3914a 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -283,7 +283,7 @@ jqUnit.test("Testing setScreenDpi ", function () { windowsDisplay.setScreenResolution(res); }); - windowsDisplay({width: 1024, height: 768}); + windowsDisplay.setScreenResolution({width: 1024, height: 768}); } // Restore the original setting at the end. From 736bad82ccbcb2e4d517a3b09cc3f17c643e744b Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Mon, 18 Sep 2017 12:04:02 -0400 Subject: [PATCH 17/21] GPII-2404: Fixed lint error --- .../displaySettingsHandler/test/testDisplaySettingsHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index bb7d3914a..941115148 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -276,7 +276,6 @@ jqUnit.test("Testing getScreenDpi ", function () { // This test is only able to test the implementation for the version of Windows it's running on. jqUnit.test("Testing setScreenDpi ", function () { // Make sure the resolution is good enough to have DPI above 100%. 1024x768 provides up to 125%. - debugger; var res = gpii.windows.display.getScreenResolution(); if (res.width < 1024 || res.height < 768) { teardowns.push(function () { From 25bc48b6ac4532fbfe7d07d75e56b0e3d2e69f9e Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Wed, 4 Oct 2017 11:04:01 -0400 Subject: [PATCH 18/21] GPII-1939: Merge upstream master GPII branch into GPII-1939_GPII-2404 --- .../WindowsUtilities/WindowsUtilities.js | 19 + .../processReporter/dotNetProcesses.csx | 44 +- .../processReporter/processesBridge.js | 15 +- .../src/RegistrySettingsHandler.js | 5 +- .../test/testRegistrySettingsHandler.js | 58 ++ .../windowsMetrics/src/windowsMetrics.js | 349 +++++++----- .../test/WindowsMetricsTests.js | 530 +++++++++--------- package.json | 1 + 8 files changed, 586 insertions(+), 435 deletions(-) diff --git a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js index 719b6700f..19a473be0 100644 --- a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js +++ b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js @@ -209,6 +209,13 @@ windows.API_constants = { WM_QUIT: 0x12, // https://msdn.microsoft.com/library/ms646281 WM_KEYUP: 0x101, + // https://msdn.microsoft.com/library/ms645616 + WM_MOUSEMOVE: 0x200, + // https://msdn.microsoft.com/library/ms645608 + WM_LBUTTONUP: 0x202, + // https://msdn.microsoft.com/library/ms646243 + WM_RBUTTONUP: 0x205, + // https://msdn.microsoft.com/library/dd375731 VK_BACK: 0x08, @@ -221,6 +228,7 @@ windows.API_constants = { LLKHF_INJECTED: 0x10, LLKHF_ALTDOWN: 0x20, LLKHF_UP: 0x80, + LLMHF_INJECTED: 0x01, // https://msdn.microsoft.com/library/ms646306 MAPVK_VK_TO_CHAR: 2, @@ -365,6 +373,17 @@ windows.KBDLLHookStruct = new Struct([ ]); windows.KBDLLHookStructPointer = ref.refType(windows.KBDLLHookStruct); +// https://msdn.microsoft.com/library/ms644970 +windows.MSDLLHookStruct = new Struct([ + [t.LONG, "ptX"], + [t.LONG, "ptY"], + [t.DWORD, "mouseData"], + [t.DWORD, "flags"], + [t.DWORD, "time"], + [t.ULONG_PTR, "dwExtraInfo"] +]); +windows.MSDLLHookStructPointer = ref.refType(windows.MSDLLHookStruct); + /** * Contains actions that can be used as the first argument of the SystemParametersInfo function. */ diff --git a/gpii/node_modules/processReporter/dotNetProcesses.csx b/gpii/node_modules/processReporter/dotNetProcesses.csx index 12cb5a8e7..1c8504a49 100644 --- a/gpii/node_modules/processReporter/dotNetProcesses.csx +++ b/gpii/node_modules/processReporter/dotNetProcesses.csx @@ -24,7 +24,7 @@ public class ProcInfo public string command = ""; public int pid = -1; public string fullPath = ""; - public string[] argv; + public string commandLine = ""; public string state = ""; } @@ -61,7 +61,7 @@ public class Startup } } } - // No process specified; gat all processes and their info + // No process specified; get all processes and their info. else { foreach (ManagementObject p in processes) { result.Add(makeProcInfo(p)); @@ -85,7 +85,8 @@ public class Startup if (propValue == null) { propValue = ""; } - procInfo.argv = makeArgv(propValue.ToString(), procInfo.fullPath); + procInfo.commandLine = propValue.ToString(); + // Should be able to get the state of the process using the // "ExecutionState" property, but that is not implemented; see: // http://maestriatech.com/wmi.php @@ -113,41 +114,4 @@ public class Startup } return procInfo; } - - public static String[] makeArgv(String commandLine, String fullPath) - { - List arguments = new List(); - - commandLine = commandLine + " "; - for (int c = 0; c < commandLine.Length; c++) { - // Handle quoted strings. - if (commandLine[c] == '"') { - int firstChar = c + 1; - int endQuoteIndex = commandLine.IndexOf('"', firstChar); - arguments.Add(commandLine.Substring(firstChar, endQuoteIndex - firstChar)); - c = endQuoteIndex; - } - // Skip spaces outside of quoted strings. - else if (commandLine[c] == ' ') { - continue; - } - // Handle non-quoted string. - else { - int firstChar = c; - int endStringIndex = commandLine.IndexOf(' ', firstChar); - arguments.Add(commandLine.Substring(firstChar, endStringIndex - firstChar)); - c = endStringIndex; - } - } - - // In some cases the first argument is just the command, e.g., "Notepad". - // In others, it is the full path and may include the ".exe" - // extension. Normalize it to always be the full path. - if (arguments.Count == 0) { - arguments.Add(fullPath); - } else { - arguments[0] = fullPath; - } - return arguments.ToArray(); - } } diff --git a/gpii/node_modules/processReporter/processesBridge.js b/gpii/node_modules/processReporter/processesBridge.js index 91458a24b..6a7ec0005 100644 --- a/gpii/node_modules/processReporter/processesBridge.js +++ b/gpii/node_modules/processReporter/processesBridge.js @@ -18,6 +18,7 @@ var path = require("path"), // This conditional require is to run the electron version of edge.js // when this code runs on electron (gpii-app). edge = process.versions.electron ? require("electron-edge") : require("edge"), + stringArgv = require("string-argv"), fluid = require("universal"); var gpii = fluid.registerNamespace("gpii"); @@ -47,5 +48,17 @@ fluid.defaults("gpii.processes.windows", { * (string), or a process id (number). */ gpii.processes.windows.getProcessList = function (processIdentifier) { - return dotNetProcesses(processIdentifier, true); + var procInfos = dotNetProcesses(processIdentifier, true); + + // Loop to convert the command line to an argv[]. + fluid.each(procInfos, function (aProcInfo) { + aProcInfo.argv = stringArgv(aProcInfo.commandLine); + delete aProcInfo.commandLine; + + // In some cases the first argument is just the command, e.g., "Notepad". + // In others, it is the full path and may include the ".exe" + // extension. Normalize it to always be the full path. + aProcInfo.argv[0] = aProcInfo.fullPath; + }); + return procInfos; }; diff --git a/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js b/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js index 30534a8c8..68668030b 100644 --- a/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js +++ b/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js @@ -269,7 +269,10 @@ windows.deleteRegistryKey = function (baseKey, path) { var pathW = windows.ensureAlignment(windows.toWideChar(regPath.realPath).pointer); var base = windows.getBaseKey(baseKey); var code = advapi32.RegDeleteKeyExW(base, pathW, regPath.desiredAccess, 0); - cr(code); + // Do not fail if delete was requested for nonexistent key + if (code !== windows.API_constants.returnCodes.FILE_NOT_FOUND) { + cr(code); + } }; /** diff --git a/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js b/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js index 1638a306e..5bd97feef 100644 --- a/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js +++ b/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js @@ -215,6 +215,32 @@ gpii.tests.windows.registrySettingsHandler.enumerationTest = { } }; +gpii.tests.windows.registrySettingsHandler.deleteKeyTest = { + initPayload: { + "settings": { + "FollowMouse": 1, + "Invert": 0, + "Magnification": 255, + "Description": "GPII Magnifier" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\GPIIMagnifier", + "dataTypes": { + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "Description": "REG_SZ" + } + }, + "count": 4 + }, + expectedReturn: { + "statusCode": 404 + } +}; + + jqUnit.test("Testing registrySettingsHandler.setImpl incl undefined value.value", function () { jqUnit.expect(1); @@ -296,6 +322,38 @@ jqUnit.test("Enumerating registry key values", function () { gpii.windows.deleteRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier"); }); +jqUnit.test("Key deletion", function () { + var testData = gpii.tests.windows.registrySettingsHandler.deleteKeyTest; + var baseKey = testData.initPayload.options.hKey; + var path = testData.initPayload.options.path; + + // Apply the data + gpii.windows.registrySettingsHandler.setImpl(testData.initPayload); + + // Delete it + gpii.windows.deleteRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier"); + + // Enumerate the key + var result = gpii.windows.enumRegistryValues(baseKey, path); + + // Check the result + jqUnit.assertDeepEq("Checking the return of enumRegistryValues", testData.expectedReturn, result); +}); + +jqUnit.test("Non-existing key deletion", function () { + jqUnit.expect(1); + try { + // Delete it + gpii.windows.deleteRegistryKey("HKEY_CURRENT_USER", "Software\\key-does-not-exist"); + } catch (e) { + jqUnit.fail("Should not fail."); + throw e; + } + + jqUnit.assert("Should succeed."); + +}); + jqUnit.test("Testing 32/64 bit views", function () { // HKEY_LOCAL_MACHINE\Software has different values for 32/64-bit processes, however this one is write-able. diff --git a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js index 8a46c8c70..52633f49b 100644 --- a/gpii/node_modules/windowsMetrics/src/windowsMetrics.js +++ b/gpii/node_modules/windowsMetrics/src/windowsMetrics.js @@ -28,7 +28,7 @@ fluid.require("%gpii-windows/gpii/node_modules/WindowsUtilities/WindowsUtilities fluid.defaults("gpii.windowsMetrics", { - gradeNames: ["fluid.modelComponent", "fluid.contextAware"], + gradeNames: ["fluid.modelComponent", "fluid.contextAware", "gpii.metrics"], contextAwareness: { platform: { checks: { @@ -44,19 +44,11 @@ fluid.defaults("gpii.windowsMetrics", { } }, listeners: { - "{lifecycleManager}.events.onSessionStop": { - "func": "{that}.events.onStopMetrics", - "priority": "before:eventLog" - }, "onDestroy.stopMetrics": "{that}.events.onStopMetrics", "onStartMetrics.application": "{that}.startApplicationMetrics", "onStopMetrics.application": "{that}.stopApplicationMetrics", - "onStartMetrics.keyboard": "{that}.startKeyboardMetrics", - "onStopMetrics.keyboard": "{that}.stopKeyboardMetrics" - }, - events: { - "onStartMetrics": null, - "onStopMetrics": null + "onStartMetrics.input": "{that}.startInputMetrics", + "onStopMetrics.input": "{that}.stopInputMetrics" }, invokers: { logMetric: { @@ -71,53 +63,55 @@ fluid.defaults("gpii.windowsMetrics", { funcName: "gpii.windows.stopApplicationMetrics", args: ["{that}"] }, - startKeyboardMetrics: { - funcName: "gpii.windows.startKeyboardMetrics", + startInputMetrics: { + funcName: "gpii.windows.startInputMetrics", args: ["{that}"] }, - stopKeyboardMetrics: { - funcName: "gpii.windows.stopKeyboardMetrics", + stopInputMetrics: { + funcName: "gpii.windows.stopInputMetrics", args: ["{that}"] } }, - model: { + members: { config: { application: { // Milliseconds to poll the active window. precision: 5000 }, - keyboard: { - // Number of records per log line (falsey for no limit). - maxRecords: 500, + input: { // Minimum typing session time, in milliseconds. minSession: 30000, // The time a session will last with no activity, in milliseconds. - sessionTimeout: 60000 + sessionTimeout: 60000, + // Minimum number of keys in a typing session time. + minSessionKeys: 30000, + // Milliseconds of no input to assume inactive + inactiveTime: 300000 } }, state: { application: {}, - keyboard: { + input: { keyboardHookHandle: null, + mouseHookHandle: null, lastKeyLogged: null, lastKeyTime: 0, - times: [] + // 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 } } } }); -fluid.defaults("gpii.windowsMetrics.windows", { - listeners: { - "{lifecycleManager}.events.onSessionStart": "{that}.events.onStartMetrics" - } -}); - -// Don't auto-start the metrics capture when testing. -fluid.defaults("gpii.windowsMetrics.test", { -}); - -fluid.defaults("gpii.eventLog.windows", { +fluid.defaults("gpii.metrics.windows", { components: { windowsMetrics: { type: "gpii.windowsMetrics" @@ -152,9 +146,9 @@ windows.getMachineID = function () { * @param that {Component} The gpii.windowsMetrics instance. */ windows.checkNewApplication = function (that) { - var runningApplications = that.model.state.application.runningApplications; - var pid = that.model.state.application.currentProcess.pid; - var exePath = that.model.state.application.currentProcess.exe; + var runningApplications = that.state.application.runningApplications; + var pid = that.state.application.currentProcess.pid; + var exePath = that.state.application.currentProcess.exe; // pid might have been re-used. var oldExe = runningApplications[pid]; @@ -192,7 +186,7 @@ windows.checkNewApplication = function (that) { * @param that {Component} The gpii.windowsMetrics instance. */ windows.startApplicationMetrics = function (that) { - that.model.state.application = { + that.state.application = { active: true, runningApplications: { count: 0 @@ -207,7 +201,7 @@ windows.startApplicationMetrics = function (that) { * @param that {Component} The gpii.windowsMetrics instance. */ windows.stopApplicationMetrics = function (that) { - that.model.state.application.active = false; + that.state.application.active = false; // Log the currently active window as though it was de-activated. windows.logAppActivate(that); }; @@ -219,10 +213,10 @@ windows.stopApplicationMetrics = function (that) { * @param that {Component} The gpii.windowsMetrics instance. */ windows.logAppActivate = function (that) { - if (that.model.state.application.currentProcess) { - var duration = process.hrtime(that.model.state.application.currentProcess.timeActivated); + if (that.state.application.currentProcess) { + var duration = process.hrtime(that.state.application.currentProcess.timeActivated); var data = { - exe: that.model.state.application.currentProcess.exe, + exe: that.state.application.currentProcess.exe, // Round to the nearest second. duration: duration[0] + (duration[1] < 5e8 ? 0 : 1) }; @@ -237,10 +231,10 @@ windows.logAppActivate = function (that) { windows.checkActiveWindow = function (that) { var activePid = 0; var lastHwnd = 0; - if (!that.model.state.application.currentProcess) { - that.model.state.application.currentProcess = {}; + if (!that.state.application.currentProcess) { + that.state.application.currentProcess = {}; } - that.model.state.application.currentProcess.timeActivated = process.hrtime(); + that.state.application.currentProcess.timeActivated = process.hrtime(); windows.waitForCondition(function () { // Get the process ID that owns the active Window. @@ -251,17 +245,17 @@ windows.checkActiveWindow = function (that) { } activePid = windows.getWindowProcessId(hwnd); } - return activePid !== that.model.state.application.currentProcess.pid || !that.model.state.application.active; + return activePid !== that.state.application.currentProcess.pid || !that.state.application.active; }, { - pollDelay: that.model.config.application.precision + pollDelay: that.config.application.precision }).then(function () { - if (that.model.state.application.active) { + if (that.state.application.active) { // Log how long the last window was active. windows.logAppActivate(that); var exePath = windows.getProcessPath(activePid); - that.model.state.application.currentProcess.pid = activePid; - that.model.state.application.currentProcess.exe = exePath; + that.state.application.currentProcess.pid = activePid; + that.state.application.currentProcess.exe = exePath; // Also perform the "application launch" metric here. A process having its window activated implies it's // been launched. @@ -272,10 +266,11 @@ windows.checkActiveWindow = function (that) { }; /** - * Starts the key stroke metrics. + * Starts the input metrics. * - * This sets up a low-level keyboard hook (WH_KEYBOARD_LL) that makes the system invoke a call-back whenever a key is - * pressed or released. (https://msdn.microsoft.com/library/ms644959) + * 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 @@ -287,31 +282,49 @@ windows.checkActiveWindow = function (that) { * * @param that {Component} The gpii.windowsMetrics instance. */ -windows.startKeyboardMetrics = function (that) { - windows.stopKeyboardMetrics(that); +windows.startInputMetrics = function (that) { + windows.stopInputMetrics(that); if (process.versions.electron) { - // The callback needs to be referenced outside here otherwise the GC will pull the rug from beneath it. + var callHook = function (code, wparam, lparam) { + // Handle the hook in the next tick, to allow the hook callback to return quickly. + process.nextTick(windows.inputHook, that, code, wparam, lparam); + 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.keyboardHookCallback = ffi.Callback( - windows.types.HANDLE, [windows.types.INT, windows.types.HANDLE, windows.KBDLLHookStructPointer], - function (code, wparam, lparam) { - windows.keyboardHook(that, code, wparam, lparam); - }); + windows.types.HANDLE, [windows.types.INT, windows.types.HANDLE, windows.KBDLLHookStructPointer], callHook); + windows.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); - that.model.state.keyboard.keyboardHookHandle = - windows.user32.SetWindowsHookExW(13, windows.keyboardHookCallback, hModule, 0); - if (!that.model.state.keyboard.keyboardHookHandle) { - windows.keyboardHookCallback = null; - fluid.fail("SetWindowsHookExW did not work. win32 error: " + windows.kernel32.GetLastError()); + // Start the keyboard hook + that.state.input.keyboardHookHandle = + windows.user32.SetWindowsHookExW(WH_KEYBOARD_LL, windows.keyboardHookCallback, hModule, 0); + + if (!that.state.input.keyboardHookHandle) { + var errCode = windows.kernel32.GetLastError(); + windows.stopInputMetrics(that); + fluid.fail("SetWindowsHookExW did not work (keyboard). win32 error: " + errCode); + } + + // Start the mouse hook + that.state.input.mouseHookHandle = + windows.user32.SetWindowsHookExW(WH_MOUSE_LL, windows.mouseHookCallback, hModule, 0); + + if (!that.state.input.mouseHookHandle) { + var errCode2 = windows.kernel32.GetLastError(); + windows.stopInputMetrics(that); + fluid.fail("SetWindowsHookExW did not work (mouse). win32 error: " + errCode2); } + windows.userInput(that); } else { // The keyboard hook's ability to work is a side-effect of running with electron. - fluid.log(fluid.logLevel.WARN, "Keyboard metrics not available without Electron."); + fluid.log(fluid.logLevel.WARN, "Input metrics not available without Electron."); } - - that.model.state.keyboard.config = that.model.config.keyboard; }; /** @@ -321,17 +334,23 @@ windows.startKeyboardMetrics = function (that) { * * @param that {Component} The gpii.windowsMetrics instance. */ -windows.stopKeyboardMetrics = function (that) { - // remove the keyboard hook - if (that.model.state.keyboard.keyboardHookHandle) { - windows.user32.UnhookWindowsHookEx(that.model.state.keyboard.keyboardHookHandle); +windows.stopInputMetrics = function (that) { + var state = that.state.input; + // remove the hooks + if (state.keyboardHookHandle) { + windows.user32.UnhookWindowsHookEx(state.keyboardHookHandle); + } + if (state.mouseHookHandle) { + windows.user32.UnhookWindowsHookEx(state.mouseHookHandle); } - that.model.state.keyboard.keyboardHookHandle = null; - that.model.state.keyboard.keyboardHookCallback = null; - // flush the log. - if (that.model.state.keyboard.times) { - windows.logKeyTimings(that); - that.model.state.keyboard.times = []; + state.keyboardHookHandle = null; + state.keyboardHookCallback = null; + state.mouseHookHandle = null; + state.mouseHookCallback = null; + + if (state.inactivityTimer) { + clearTimeout(state.inactivityTimer); + state.inactivityTimer = null; } }; @@ -354,60 +373,62 @@ windows.specialKeys = fluid.freezeRecursive((function () { return special; })()); -/** - * Sends the key timings to the log. - * - * @param times {Array} Array of key times. - * @param maxRecords {Number} Maximum key times per log entry. - */ -windows.logKeyTimings = function (that) { - var times = that.model.state.keyboard.times; - var maxRecords = that.model.config.keyboard.maxRecords || times.length; - - for (var offset = 0; offset < times.length; offset += maxRecords) { - // Split the list of timings into chunks. - var data = { - times: times.slice(offset, offset + maxRecords) - }; - that.logMetric("key-times", data); - } -}; - /** * 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 state {Object} The keyboard metrics state. + * @param that {Component} The gpii.windowsMetrics instance. * @param timestamp {Number} Milliseconds since a fixed point in time. * @param specialKey {String} The value key, if it's a special key. */ windows.recordKeyTiming = function (that, timestamp, specialKey) { - var state = that.model.state.keyboard; - if (!state.times) { - state.times = []; - } + 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.times.length === 1) && (keyTime > state.config.minSession)) { - // This key was hit after the minimum session time, so ignore the last key. - state.times = []; - } else if ((state.times.length > 1) && (keyTime > state.config.sessionTimeout)) { - // Store the current session, and begin a new one. - windows.logKeyTimings(that); - state.times = []; - keyTime = 0; + 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 = { - t: keyTime + keyTime: keyTime }; if (specialKey) { @@ -417,7 +438,66 @@ windows.recordKeyTiming = function (that, timestamp, specialKey) { } } - state.times.push(record); + that.logMetric("key-time", record); +}; + +/** + * Records a mouse event. Movement isn't logged, but the distance is accumulated. + * + * @param that {Component} The gpii.windowsMetrics instance. + * @param eventType {String} "move" or "button". + * @param button {Number} Mouse button: 0 movement only, 1 for primary (left) or 2 or secondary. + * @param pos {Object} mouse cursor coordinates {x, y}. + */ +windows.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; + + if (button) { + that.logMetric("mouse", { + button: button, + distance: Math.round(state.distance) + }); + state.distance = 0; + } +}; + +/** + * Called by inputHook when it receives some input, then waits for no further input to detect inactivity. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.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.userInactive, that.config.input.inactiveTime, that); +}; + +/** + * Called when there's been some time since receiving input from the user. + * + * @param that {Component} The gpii.windowsMetrics instance. + */ +windows.userInactive = function (that) { + that.state.input.inactive = true; + that.logMetric("inactive-begin"); }; /** @@ -426,42 +506,55 @@ windows.recordKeyTiming = function (that, timestamp, specialKey) { * * @param that {Component} The gpii.windowsMetrics instance. * @param code {Number} If less than zero, then don't process. - * @param wparam {Number} The keyboard message. One of WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP - * @param lparam {Number} A pointer to a KBDLLHOOKSTRUCT structure. + * @param wparam {Number} The input message (eg, WM_KEYDOWN or WM_MOUSEMOVE). + * @param lparam {Number} A pointer to a KBDLLHOOKSTRUCT or MSDLLHOOKSTRUCT structure. * @return {Number} The return value of CallNextHookEx. */ -windows.keyboardHook = function (that, code, wparam, lparam) { +windows.inputHook = function (that, code, wparam, lparam) { var togo; - try { - if (code >= 0 && wparam === windows.API_constants.WM_KEYUP) { + if (code >= 0) { + windows.userInput(that); + + // For testing, allows the object to be passed directly rather than a ffi/ref struct. + var eventData = lparam.deref ? lparam.deref() : lparam; + + switch (wparam) { + case windows.API_constants.WM_KEYUP: + // Key press var wanted = false; - // For testing, allows the object to be passed directly rather than a ffi/ref struct. - var kb = lparam.deref ? lparam.deref() : lparam; - var specialKey = windows.specialKeys[kb.vkCode]; + var specialKey = windows.specialKeys[eventData.vkCode]; // Ignore injected and Alt keys. var ignoreFlags = windows.API_constants.LLKHF_INJECTED | windows.API_constants.LLKHF_ALTDOWN; - if ((kb.flags & ignoreFlags) === 0) { + if ((eventData.flags & ignoreFlags) === 0) { // If the key doesn't generate a character, then don't count it. - wanted = specialKey || windows.user32.MapVirtualKeyW(kb.vkCode, windows.API_constants.MAPVK_VK_TO_CHAR); - if (kb.vkCode === windows.API_constants.VK_LEFT) { + wanted = specialKey || windows.user32.MapVirtualKeyW(eventData.vkCode, windows.API_constants.MAPVK_VK_TO_CHAR); + if (eventData.vkCode === windows.API_constants.VK_LEFT) { // Special case for the 'left' cursor key - if the last key was logged then log this key as it was // probably used for correcting a mistake. - wanted = that.model.state.keyboard.lastKeyLogged; + wanted = that.state.input.lastKeyLogged; } if (wanted) { // Process in the next tick, to allow this function to return soon. - process.nextTick(windows.recordKeyTiming, that, kb.time, specialKey); + windows.recordKeyTiming(that, eventData.time, specialKey); } - that.model.state.keyboard.lastKeyLogged = wanted && !specialKey; + that.state.input.lastKeyLogged = wanted && !specialKey; + } + break; + + case windows.API_constants.WM_MOUSEMOVE: + case windows.API_constants.WM_LBUTTONUP: + case windows.API_constants.WM_RBUTTONUP: + // Don't log injected events. + if ((eventData.flags & windows.API_constants.LLMHF_INJECTED) === 0) { + var button = (wparam >> 1) & 3; // 2nd + 3rd bits happen to map to the button + windows.recordMouseEvent(that, button, { x: eventData.ptX, y: eventData.ptY }); } + break; } - } finally { - // This needs to be called, or the key will be lost. - togo = windows.user32.CallNextHookEx(0, code, wparam, lparam); } return togo; }; diff --git a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js index b199777de..ae484eae8 100644 --- a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js +++ b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js @@ -41,118 +41,17 @@ jqUnit.module("gpii.tests.metrics", { } }); -//fluid.logObjectRenderChars = 0xffff; - // Wrap the gpii.windowsMetrics to disable auto-starting the metrics capturing. fluid.defaults("gpii.tests.metrics.windowsMetricsWrapper", { gradeNames: ["fluid.component", "gpii.windowsMetrics", "gpii.eventLog", "gpii.lifecycleManager"], listeners: { "onStartMetrics.application": null, - "onStartMetrics.keyboard": null, + "onStartMetrics.input": null, "onStopMetrics.application": null, - "onStopMetrics.keyboard": null + "onStopMetrics.input": null } }); -// Tests logKeyTimings, splitting of key timings into separate log entries. -gpii.tests.metrics.logKeyTimingsTests = fluid.freezeRecursive([ - { // Single record - input: { - times: [{t: 10}], - maxRecords: 1 - }, - expect: { - module: "metrics", - event: "key-times", - data: { times: [ {t: 10} ] } - } - }, - { // Multiple records, 1 line each - input: { - times: [{t: 20}, {t: 21}, {t: 22}], - maxRecords: 1 - }, - expect: [ - { - module: "metrics", - event: "key-times", - data: { times: [{t: 20}] } - }, - { - module: "metrics", - event: "key-times", - data: { times: [{t: 21}] } - }, - { - module: "metrics", - event: "key-times", - data: { times: [{t: 22}] } - } - ] - }, - { // 2 sets of 3 records - input: { - times: [{t: 30}, {t: 31}, {t: 32}, {t: 33}, {t: 34}, {t: 35}], - maxRecords: 3 - }, - expect: [ - { - module: "metrics", - event: "key-times", - data: { - times: [{t: 30}, {t: 31}, {t: 32}] - } - }, - { - module: "metrics", - event: "key-times", - data: { - times: [{t: 33}, {t: 34}, {t: 35}] - } - } - ] - }, - { - // Imperfect number - input: { - times: [{t: 40}, {t: 41}, {t: 42}, {t: 43}, {t: 44}], - maxRecords: 3 - }, - expect: [ - { - module: "metrics", - event: "key-times", - data: { - times: [{t: 40}, {t: 41}, {t: 42}] - } - }, - { - module: "metrics", - event: "key-times", - data: { - times: [{t: 43}, {t: 44}] - } - } - ] - }, - { - // No cut-off - input: { - times: [{t: 50}, {t: 51}, {t: 52}, {t: 53}, {t: 54}, {t: 55}, {t: 56}, {t: 57}, {t: 58}, {t: 59}], - maxRecords: null - }, - expect: [ - { - module: "metrics", - event: "key-times", - data: { - times: [{t: 50}, {t: 51}, {t: 52}, {t: 53}, {t: 54}, {t: 55}, {t: 56}, {t: 57}, {t: 58}, {t: 59}] - } - } - ] - } -]); - gpii.tests.metrics.defaultKeyboardConfig = fluid.freezeRecursive(); // Tests for recordKeyTiming: key presses @@ -166,7 +65,7 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ maxRecords: 1000 }, testData: [ - { // First key press + { // Normal key press state: {}, input: { timestamp: 1, @@ -174,49 +73,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0}]} - } - }, - { // Key press, existing state - state: null, - input: { - timestamp: 1, - key: undefined - }, - expect: { - module: "metrics", - event: "key-times", - data: {times: [{t: 0}]} - } - }, - { // Key press, existing state with previous keys - state: { - times: [{t:10}, {t:11}, {t:12} ] - }, - input: { - timestamp: 1, - key: undefined - }, - expect: { - module: "metrics", - event: "key-times", - data: {times: [{t: 10}, {t: 11}, {t: 12}, {t: 0} ]} - } - }, - { // Key press, existing state with previous keys and lastKeyTime set - state: { - times: [{t:20}, {t:21}, {t:22}], - lastKeyTime: 100 - }, - input: { - timestamp: 200, - key: undefined - }, - expect: { - module: "metrics", - event: "key-times", - data: {times: [{t: 20}, {t: 21}, {t: 22}, {t: 100} ]} + event: "key-time", + data: {keyTime: 0} } }, { // special key @@ -226,8 +84,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: "BS"}]} + event: "key-time", + data: {keyTime: 0, key: "BS"} } }, // Test that non-special keys don't get leaked. @@ -238,8 +96,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: fluid.NO_VALUE }]} + event: "key-time", + data: {keyTime: 0, key: fluid.NO_VALUE } } }, { // not a special key (symbol) @@ -249,8 +107,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: fluid.NO_VALUE }]} + event: "key-time", + data: {keyTime: 0, key: fluid.NO_VALUE} } }, { // not a special key (numeric string) @@ -260,8 +118,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: fluid.NO_VALUE }]} + event: "key-time", + data: {keyTime: 0, key: fluid.NO_VALUE} } }, { // not a special key (number) @@ -271,8 +129,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: fluid.NO_VALUE }]} + event: "key-time", + data: {keyTime: 0, key: fluid.NO_VALUE} } }, { // not a special key (string) @@ -282,8 +140,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: fluid.NO_VALUE }]} + event: "key-time", + data: {keyTime: 0, key: fluid.NO_VALUE} } }, { // not a special key (virtual key code of a special key) @@ -293,8 +151,8 @@ gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({ }, expect: { module: "metrics", - event: "key-times", - data: {times: [{t: 0, key: fluid.NO_VALUE }]} + event: "key-time", + data: {keyTime: 0, key: fluid.NO_VALUE} } } ] @@ -306,56 +164,95 @@ gpii.tests.metrics.typingSessionTests = fluid.freezeRecursive({ times: [] }, defaultConfig: { - minSession: 30, - sessionTimeout: 60, - maxRecords: 1000 + minSession: 30000, + sessionTimeout: 60000, + minSessionKeys: 3 }, testData: [ - { // Single session - state: { - times: [] - }, + { // Single session (3 keys, 1 minute) + state: {}, input: [ { timestamp: 1 }, - { timestamp: 2 }, - { timestamp: 4 }, - { timestamp: 8 } + { timestamp: 20000 }, + { timestamp: 60001 }, + { timestamp: 200000 } ], - expect: { + expect: [{ module: "metrics", - event: "key-times", - data: {times: [{t: 0}, {t: 1}, {t: 2}, {t: 4} ]} - } + event: "typing-session", + data: { + duration: 60000, + count: 3, + corrections: 0, + rate: 3 + } + }] + }, + { // Single session (10 keys, 5 minutes) + state: {}, + input: [ + { timestamp: 1 }, + { timestamp: 60000 }, + { timestamp: 90000 }, + { timestamp: 120000 }, + { timestamp: 150000 }, + { timestamp: 180000 }, + { timestamp: 210000 }, + { timestamp: 240000 }, + { timestamp: 270000 }, + { timestamp: 300001 }, + { timestamp: 9000000 } + ], + expect: [{ + module: "metrics", + event: "typing-session", + data: { + duration: 300000, + count: 10, + corrections: 0, + rate: 2 + } + }] }, { // Two sessions - state: { - times: [] - }, + state: {}, input: [ - {timestamp: 1}, - {timestamp: 2}, - {timestamp: 3}, - {timestamp: 4}, - {timestamp: 200}, - {timestamp: 202}, - {timestamp: 204}, - {timestamp: 206} + // 1st + { timestamp: 1 }, + { timestamp: 20000 }, + { timestamp: 60001 }, + // 2nd + { timestamp: 200000 }, + { timestamp: 250000 }, + { timestamp: 300000 }, + { timestamp: 350000 }, + { timestamp: 9000000 } ], expect: [{ module: "metrics", - event: "key-times", - data: {times: [{t: 0}, {t: 1}, {t: 1}, {t: 1}]} + event: "typing-session", + data: { + duration: 60000, + count: 3, + corrections: 0, + rate: 3 + } }, { module: "metrics", - event: "key-times", - data: {times: [{t: 0}, {t: 2}, {t: 2}, {t: 2}]} + event: "typing-session", + data: { + duration: 150000, + count: 4, + corrections: 0, + rate: 2 + } }] } ] }); -// Tests for keyboardHook. -gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ +// Tests for inputHook. +gpii.tests.metrics.inputHookTests = fluid.freezeRecursive([ { // Invalid nCode. input: { nCode: -1, @@ -384,8 +281,8 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 0, key: fluid.NO_VALUE} ] } + event: "key-time", + data: { keyTime: 0, key: fluid.NO_VALUE } } }, { // Next key (50ms later). @@ -402,8 +299,8 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 50, key: fluid.NO_VALUE} ] } + event: "key-time", + data: { keyTime: 50, key: fluid.NO_VALUE } } }, { // Non-special non-input key. @@ -434,8 +331,8 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 0, key: "BS"} ] } + event: "key-time", + data: { keyTime: 0, key: "BS" } } }, { // Special key: delete. @@ -452,8 +349,8 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 0, key: "DEL"} ] } + event: "key-time", + data: { keyTime: 0, key: "DEL" } } }, { // Special key: escape. @@ -470,8 +367,8 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 0, key: "ESC"} ] } + event: "key-time", + data: { keyTime: 0, key: "ESC" } } }, { // Special key: left arrow. Expect nothing, because this only logs if hit after a character key. @@ -502,8 +399,8 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 0, key: fluid.NO_VALUE } ] } + event: "key-time", + data: { keyTime: 0, key: fluid.NO_VALUE } } }, { // Special key: left arrow. @@ -520,8 +417,122 @@ gpii.tests.metrics.keyboardHookTests = fluid.freezeRecursive([ }, expect: { module: "metrics", - event: "key-times", - data: { times: [ {t: 0, key: "LEFT"} ] } + event: "key-time", + data: { keyTime: 0, key: "LEFT" } + } + }, + // Mouse events + { // Click + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_LBUTTONUP, + lParam: { + ptX: 0, + ptY: 0, + mouseData: 0, + flags: 0, + time: 0, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "mouse", + data: { button: 1, distance: 0 } + } + }, + { // Right click + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_RBUTTONUP, + lParam: { + ptX: 0, + ptY: 0, + mouseData: 0, + flags: 0, + time: 0, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "mouse", + data: { button: 2, distance: 0 } + } + }, + { // Click with movement + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_LBUTTONUP, + lParam: { + ptX: 100, + ptY: 200, + mouseData: 0, + flags: 0, + time: 0, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "mouse", + data: { button: 1, distance: 224 } + } + }, + { // Click with movement again - distance should be from the last coordinate. + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_RBUTTONUP, + lParam: { + ptX: 100 + 50, + ptY: 200 + 60, + mouseData: 0, + flags: 0, + time: 0, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "mouse", + // sqrt(50^2 + 60^2) = 78 + data: { button: 2, distance: 78 } + } + }, + { // Just movement + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_MOUSEMOVE, + lParam: { + ptX: 150 - 25, + ptY: 260 - 30, + mouseData: 0, + flags: 0, + time: 0, + dwExtraInfo: 0 + } + }, + // Shouldn't produce anything + expect: [] + }, + { // Click + more movement + input: { + nCode: 0, + wParam: gpii.windows.API_constants.WM_LBUTTONUP, + lParam: { + ptX: 125 + 10, + ptY: 230 + 20, + mouseData: 0, + flags: 0, + time: 0, + dwExtraInfo: 0 + } + }, + expect: { + module: "metrics", + event: "mouse", + // (-25,-30) + (10,20) => 39 + 22 = 61 + data: { button: 1, distance: 61 } } } ]); @@ -537,7 +548,11 @@ gpii.tests.metrics.setLogFile = function () { var logFile = os.tmpdir() + "/gpii-test-metrics-" + Date.now(); teardowns.push(function () { gpii.eventLog.logFilePath = previousLogFile; - fs.unlinkSync(logFile); + try { + fs.unlinkSync(logFile); + } catch (e) { + // Ignored. + } }); gpii.eventLog.logFilePath = logFile; return logFile; @@ -675,7 +690,7 @@ jqUnit.asyncTest("Testing application metrics", function () { } }); - windowsMetrics.model.config.application.precision = 1; + windowsMetrics.config.application.precision = 1; // Pick three windows (owned by different processes) that already exist, and pretend they've became active by // mocking GetForegroundWindow. @@ -744,29 +759,6 @@ jqUnit.asyncTest("Testing application metrics", function () { windowsMetrics.startApplicationMetrics(); }); -jqUnit.asyncTest("Testing keyboard metrics: logKeyTimings", function () { - jqUnit.expect(1); - - var logFile = gpii.tests.metrics.setLogFile(); - var testData = gpii.tests.metrics.logKeyTimingsTests; - var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({ - members: { - logFilePath: logFile - } - }); - - var expectedLines = []; - - fluid.each(testData, function (test) { - windowsMetrics.model.state.keyboard.times = test.input.times; - windowsMetrics.model.config.keyboard.maxRecords = test.input.maxRecords; - gpii.windows.logKeyTimings(windowsMetrics); - expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect)); - }); - - gpii.tests.metrics.completeLogTest(logFile, expectedLines); -}); - /** * Test recordKeyTiming * @param theTests @@ -790,17 +782,14 @@ gpii.tests.metrics.recordKeyTimingTests = function (theTests) { fluid.each(testData, function (test) { var state = fluid.copy(test.state || defaultState); state.config = fluid.copy(state.config || defaultConfig); - windowsMetrics.model.state.keyboard = state; - windowsMetrics.model.config.keyboard = state.config; + windowsMetrics.state.input = state; + windowsMetrics.config.input = state.config; // "press" the keys fluid.each(fluid.makeArray(test.input), function (input) { gpii.windows.recordKeyTiming(windowsMetrics, input.timestamp, input.key); }); - // flush the log - gpii.windows.logKeyTimings(windowsMetrics); - expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect)); }); @@ -815,7 +804,8 @@ jqUnit.asyncTest("Testing keyboard metrics: recordKeyTiming (typing sessions)", gpii.tests.metrics.recordKeyTimingTests(gpii.tests.metrics.typingSessionTests); }); -jqUnit.asyncTest("Testing keyboard metrics: keyboardHook", function () { +// This also tests recordMouseEvent (indirectly) +jqUnit.asyncTest("Testing input metrics: inputHook", function () { jqUnit.expect(1); var logFile = gpii.tests.metrics.setLogFile(); @@ -827,46 +817,56 @@ jqUnit.asyncTest("Testing keyboard metrics: keyboardHook", function () { }); // Disable typing sessions - windowsMetrics.model.config.keyboard.minSession = windowsMetrics.model.config.keyboard.sessionTimeout = -1 >>> 0; - windowsMetrics.model.config.keyboard.maxRecords = 1; - windowsMetrics.startKeyboardMetrics(); + windowsMetrics.config.input.minSession = windowsMetrics.config.input.sessionTimeout = -1 >>> 0; + windowsMetrics.config.input.maxRecords = 1; + windowsMetrics.startInputMetrics(); - var testData = gpii.tests.metrics.keyboardHookTests; + var testData = gpii.tests.metrics.inputHookTests; var expectedLines = []; var currentTest = null; - var nextHookReturn = 0; - - // Mock CallNextHookEx - this must be called for every call to the hook handler with the same parameters. - var origCallNextHookEx = gpii.windows.user32.CallNextHookEx; - gpii.windows.user32.CallNextHookEx = function (hhk, nCode, wParam, lParam) { - // hhk is ignored by windows, but check it's 0 because that's the code's intended value. - jqUnit.assertEquals("CallNextHookEx should be called with the correct hhk", 0, hhk); - jqUnit.assertEquals("CallNextHookEx must be called with the correct nCode", currentTest.nCode, nCode); - jqUnit.assertEquals("CallNextHookEx must be called with the correct wParam", currentTest.wParam, wParam); - jqUnit.assertDeepEq("CallNextHookEx must be called with the correct lParam", currentTest.lParam, lParam); - return ++nextHookReturn; - }; - teardowns.push(function () { - gpii.windows.user32.CallNextHookEx = origCallNextHookEx; - }); - - // expect the CallNextHookEx calls (1 assert for each param + the return). - jqUnit.expect(testData.length * 5); fluid.each(testData, function (test) { currentTest = test.input; - var ret = gpii.windows.keyboardHook.call( - null, windowsMetrics, currentTest.nCode, currentTest.wParam, currentTest.lParam); - - jqUnit.assertEquals("keyboardHook must return the result of CallNextHookEx", nextHookReturn, ret); + gpii.windows.inputHook(windowsMetrics, currentTest.nCode, currentTest.wParam, currentTest.lParam); expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect)); }); - // The keys are put in the log in the next tick (to allow the hook handler to return quickly). + // The events are put in the log in the next tick (to allow the hook handler to return quickly). setImmediate(function () { - windowsMetrics.stopKeyboardMetrics(); + windowsMetrics.stopInputMetrics(); gpii.tests.metrics.completeLogTest(logFile, expectedLines); }); }); + +jqUnit.asyncTest("Testing input metrics: inactivity", function () { + jqUnit.expect(4); + var logFile = gpii.tests.metrics.setLogFile(); + + var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({ + members: { + logFilePath: logFile + } + }); + + // Make the inactivity timeout very quick + windowsMetrics.config.input.inactiveTime = 1; + windowsMetrics.startInputMetrics(); + var state = windowsMetrics.state.input; + + jqUnit.assertFalse("Should not be inactive at start", state.inactive); + + gpii.windows.userInput(windowsMetrics); + + jqUnit.assertFalse("Should not be inactive after first input", state.inactive); + + setTimeout(function () { + jqUnit.assertTrue("Should be inactive after timer", state.inactive); + gpii.windows.userInput(windowsMetrics); + jqUnit.assertFalse("Should not be inactive after last input", state.inactive); + + windowsMetrics.stopInputMetrics(); + jqUnit.start(); + }, 10); +}); diff --git a/package.json b/package.json index 542c57d4a..74bf093bd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "ref-struct": "1.1.0", "ref-array": "1.1.2", "edge": "6.5.1", + "string-argv": "0.0.2", "universal": "klown/universal#GPII-1939" }, "devDependencies": { From e62f6453c0141835bb7b8e0a8c3367aaf2c62f45 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Tue, 20 Feb 2018 12:03:35 -0500 Subject: [PATCH 19/21] GPII-1939: Merge branch 'GPII-2404' into GPII-1939_GPII-2404 - includes a merge of upstream master GPII branch - adjusted "require()" statements in PlatformReporter code to use "gpii-universal" instaed of "universal" or "%universal". --- gpii.js | 2 +- .../WindowsUtilities/WindowsUtilities.js | 4 +-- .../src/displaySettingsHandler.js | 2 +- .../src/dpiWindows10.js | 2 +- .../displaySettingsHandler/src/dpiWindows8.js | 2 +- .../test/testDisplaySettingsHandler.js | 2 +- .../platformReporter/PlatformReporter.js | 2 +- .../test/PlatformReporterTests.js | 2 +- .../processHandling/processHandling.js | 2 +- .../test/testProcessHandling.js | 2 +- .../processReporter/processReporter.js | 2 +- .../processReporter/processesBridge.js | 2 +- .../processReporter/test/all-tests.js | 2 +- .../test/processReporterModuleTests.js | 2 +- .../test/processesBridge_tests.js | 2 +- .../node_modules/registeredAT/registeredAT.js | 2 +- .../registeredAT/test/testRegisteredAT.js | 2 +- gpii/node_modules/registryResolver/index.js | 2 +- .../registryResolver/src/RegistryResolver.js | 2 +- .../test/testRegistryResolver.js | 2 +- .../src/RegistrySettingsHandler.js | 11 ++++--- .../test/testRegistrySettingsHandler.js | 33 ++++++++++++++++--- .../src/SpiSettingsHandler.js | 2 +- .../test/testSpiSettingsHandler.js | 2 +- .../test/WindowsMetricsTests.js | 2 +- index.js | 2 +- package.json | 2 +- tests/AcceptanceTests.js | 6 ++-- 28 files changed, 64 insertions(+), 38 deletions(-) diff --git a/gpii.js b/gpii.js index 75d0d431d..c8c6d7648 100644 --- a/gpii.js +++ b/gpii.js @@ -16,7 +16,7 @@ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), gpii = fluid.registerNamespace("gpii"); require("./index.js"); diff --git a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js index 19a473be0..7931d1b0d 100644 --- a/gpii/node_modules/WindowsUtilities/WindowsUtilities.js +++ b/gpii/node_modules/WindowsUtilities/WindowsUtilities.js @@ -19,7 +19,7 @@ "use strict"; var ffi = require("ffi"); -var fluid = require("universal"); +var fluid = require("gpii-universal"); var gpii = fluid.registerNamespace("gpii"); var windows = fluid.registerNamespace("gpii.windows"); @@ -542,7 +542,7 @@ windows.toWideChar = function (string) { buffer.writeInt16BE(0, chars * 2); // add the null character at the end return { pointer: buffer, - length: chars * 2 + length: chars * 2 + 2 }; }; diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js index 057e5f400..c867b7ab5 100644 --- a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -16,7 +16,7 @@ var ref = require("ref"); var Struct = require("ref-struct"); var arrayType = require("ref-array"); var ffi = require("ffi"); -var fluid = require("universal"); +var fluid = require("gpii-universal"); var os = require("os"); var semver = require("semver"); diff --git a/gpii/node_modules/displaySettingsHandler/src/dpiWindows10.js b/gpii/node_modules/displaySettingsHandler/src/dpiWindows10.js index ccf9c9b50..03987acc9 100644 --- a/gpii/node_modules/displaySettingsHandler/src/dpiWindows10.js +++ b/gpii/node_modules/displaySettingsHandler/src/dpiWindows10.js @@ -43,7 +43,7 @@ var ref = require("ref"); var Struct = require("ref-struct"); var arrayType = require("ref-array"); var ffi = require("ffi"); -var fluid = require("universal"); +var fluid = require("gpii-universal"); var windows = fluid.registerNamespace("gpii.windows"); diff --git a/gpii/node_modules/displaySettingsHandler/src/dpiWindows8.js b/gpii/node_modules/displaySettingsHandler/src/dpiWindows8.js index da4e5fdc4..9fb8a29ea 100644 --- a/gpii/node_modules/displaySettingsHandler/src/dpiWindows8.js +++ b/gpii/node_modules/displaySettingsHandler/src/dpiWindows8.js @@ -12,7 +12,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var windows = fluid.registerNamespace("gpii.windows"); diff --git a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js index 941115148..1a95e59e2 100644 --- a/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js +++ b/gpii/node_modules/displaySettingsHandler/test/testDisplaySettingsHandler.js @@ -12,7 +12,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var jqUnit = fluid.require("node-jqunit"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/platformReporter/PlatformReporter.js b/gpii/node_modules/platformReporter/PlatformReporter.js index 6245d2d58..ff44badea 100644 --- a/gpii/node_modules/platformReporter/PlatformReporter.js +++ b/gpii/node_modules/platformReporter/PlatformReporter.js @@ -20,7 +20,7 @@ require("../WindowsUtilities/WindowsUtilities.js"); require("displaySettingsHandler"); var gpii = fluid.registerNamespace("gpii"); -fluid.require("%universal"); +fluid.require("gpii-universal"); fluid.defaults("gpii.platformReporter.windows", { gradeNames: ["gpii.platformReporter"], diff --git a/gpii/node_modules/platformReporter/test/PlatformReporterTests.js b/gpii/node_modules/platformReporter/test/PlatformReporterTests.js index 780cbc1cc..68b72b7ce 100644 --- a/gpii/node_modules/platformReporter/test/PlatformReporterTests.js +++ b/gpii/node_modules/platformReporter/test/PlatformReporterTests.js @@ -15,7 +15,7 @@ */ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), os = require("os"), gpii = fluid.registerNamespace("gpii"), jqUnit = fluid.require("node-jqunit"); diff --git a/gpii/node_modules/processHandling/processHandling.js b/gpii/node_modules/processHandling/processHandling.js index d8b196580..505a9d9ee 100644 --- a/gpii/node_modules/processHandling/processHandling.js +++ b/gpii/node_modules/processHandling/processHandling.js @@ -17,7 +17,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var ref = require("ref"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/processHandling/test/testProcessHandling.js b/gpii/node_modules/processHandling/test/testProcessHandling.js index fb2a00220..24e692e94 100644 --- a/gpii/node_modules/processHandling/test/testProcessHandling.js +++ b/gpii/node_modules/processHandling/test/testProcessHandling.js @@ -16,7 +16,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var jqUnit = fluid.require("node-jqunit"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/processReporter/processReporter.js b/gpii/node_modules/processReporter/processReporter.js index dbf43ca6a..8f292bc3e 100644 --- a/gpii/node_modules/processReporter/processReporter.js +++ b/gpii/node_modules/processReporter/processReporter.js @@ -12,7 +12,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var gpii = fluid.registerNamespace("gpii"); require("./processesBridge.js"); diff --git a/gpii/node_modules/processReporter/processesBridge.js b/gpii/node_modules/processReporter/processesBridge.js index 6a7ec0005..79928ca71 100644 --- a/gpii/node_modules/processReporter/processesBridge.js +++ b/gpii/node_modules/processReporter/processesBridge.js @@ -19,7 +19,7 @@ var path = require("path"), // when this code runs on electron (gpii-app). edge = process.versions.electron ? require("electron-edge") : require("edge"), stringArgv = require("string-argv"), - fluid = require("universal"); + fluid = require("gpii-universal"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/processReporter/test/all-tests.js b/gpii/node_modules/processReporter/test/all-tests.js index 99db881f9..fff9138df 100644 --- a/gpii/node_modules/processReporter/test/all-tests.js +++ b/gpii/node_modules/processReporter/test/all-tests.js @@ -11,7 +11,7 @@ */ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), kettle = fluid.require("kettle"); kettle.loadTestingSupport(); diff --git a/gpii/node_modules/processReporter/test/processReporterModuleTests.js b/gpii/node_modules/processReporter/test/processReporterModuleTests.js index d379fe22c..197d7e608 100644 --- a/gpii/node_modules/processReporter/test/processReporterModuleTests.js +++ b/gpii/node_modules/processReporter/test/processReporterModuleTests.js @@ -11,7 +11,7 @@ */ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), jqUnit = fluid.require("node-jqunit"); require("processReporter"); diff --git a/gpii/node_modules/processReporter/test/processesBridge_tests.js b/gpii/node_modules/processReporter/test/processesBridge_tests.js index a06ce1dec..914364904 100644 --- a/gpii/node_modules/processReporter/test/processesBridge_tests.js +++ b/gpii/node_modules/processReporter/test/processesBridge_tests.js @@ -16,7 +16,7 @@ https://github.com/gpii/universal/LICENSE.txt var path = require("path"), spawn = require("child_process").spawn, - fluid = require("universal"), + fluid = require("gpii-universal"), jqUnit = fluid.require("node-jqunit"); require("../processesBridge.js"); diff --git a/gpii/node_modules/registeredAT/registeredAT.js b/gpii/node_modules/registeredAT/registeredAT.js index 050e65ecb..6f5302671 100644 --- a/gpii/node_modules/registeredAT/registeredAT.js +++ b/gpii/node_modules/registeredAT/registeredAT.js @@ -17,7 +17,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var gpii = fluid.registerNamespace("gpii"); var child_process = require("child_process"); diff --git a/gpii/node_modules/registeredAT/test/testRegisteredAT.js b/gpii/node_modules/registeredAT/test/testRegisteredAT.js index ce9e673b3..134d1765b 100644 --- a/gpii/node_modules/registeredAT/test/testRegisteredAT.js +++ b/gpii/node_modules/registeredAT/test/testRegisteredAT.js @@ -16,7 +16,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var jqUnit = fluid.require("node-jqunit"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/registryResolver/index.js b/gpii/node_modules/registryResolver/index.js index 457ead05c..91c1df29e 100644 --- a/gpii/node_modules/registryResolver/index.js +++ b/gpii/node_modules/registryResolver/index.js @@ -1,6 +1,6 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); require("./src/RegistryResolver.js"); diff --git a/gpii/node_modules/registryResolver/src/RegistryResolver.js b/gpii/node_modules/registryResolver/src/RegistryResolver.js index 79804c1a9..a06aabec7 100644 --- a/gpii/node_modules/registryResolver/src/RegistryResolver.js +++ b/gpii/node_modules/registryResolver/src/RegistryResolver.js @@ -16,7 +16,7 @@ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), gpii = fluid.registerNamespace("gpii"); require("../../registrySettingsHandler/src/RegistrySettingsHandler.js"); diff --git a/gpii/node_modules/registryResolver/test/testRegistryResolver.js b/gpii/node_modules/registryResolver/test/testRegistryResolver.js index cb88d71d0..a78e81966 100644 --- a/gpii/node_modules/registryResolver/test/testRegistryResolver.js +++ b/gpii/node_modules/registryResolver/test/testRegistryResolver.js @@ -16,7 +16,7 @@ "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); var jqUnit = fluid.require("node-jqunit"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js b/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js index 68668030b..1cdecd437 100644 --- a/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js +++ b/gpii/node_modules/registrySettingsHandler/src/RegistrySettingsHandler.js @@ -20,7 +20,7 @@ var ffi = require("ffi"), ref = require("ref"), - fluid = require("universal"); + fluid = require("gpii-universal"); require("../../WindowsUtilities/WindowsUtilities.js"); @@ -212,7 +212,9 @@ windows.readRegistryKey = function (baseKey, path, subKey, type) { cr(code2); togo.bytes = dataLength.readInt32LE(0); - var valueHolder = new Buffer(togo.bytes); + // Add an extra 2 bytes to ensure the return is always null terminated (numeric values will ignore it). + var valueHolder = new Buffer(togo.bytes + 2); + valueHolder.fill(0); var code3 = advapi32.RegQueryValueExW(keyHolder.readUInt32LE(0), subKeyW, NULL, NULL, valueHolder, dataLength); cr(code3); @@ -318,8 +320,9 @@ windows.enumRegistryValues = function (baseKey, path) { togo[value.name] = value; - // Get the data for the value. - var dataHolder = new Buffer(value.bytes); + // Add an extra 2 bytes to ensure the return is always null terminated (numeric values will ignore it). + var dataHolder = new Buffer(value.bytes + 2); + dataHolder.fill(0); var code3 = advapi32.RegQueryValueExW(keyHolder.readUInt32LE(0), nameHolder, NULL, typeHolder, dataHolder, dataSizeHolder); cr(code3); diff --git a/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js b/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js index 5bd97feef..ad0efd064 100644 --- a/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js +++ b/gpii/node_modules/registrySettingsHandler/test/testRegistrySettingsHandler.js @@ -14,7 +14,7 @@ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), os = require("os"); var jqUnit = fluid.require("node-jqunit"); @@ -173,7 +173,8 @@ gpii.tests.windows.registrySettingsHandler.enumerationTest = { "FollowMouse": 1, "Invert": 0, "Magnification": 255, - "Description": "GPII Magnifier" + "Description": "GPII Magnifier", + "ZeroLength": "" }, "options": { "hKey": "HKEY_CURRENT_USER", @@ -182,10 +183,11 @@ gpii.tests.windows.registrySettingsHandler.enumerationTest = { "FollowMouse": "REG_DWORD", "Invert": "REG_DWORD", "Magnification": "REG_DWORD", - "Description": "REG_SZ" + "Description": "REG_SZ", + "ZeroLength": "REG_SZ" } }, - "count": 4 + "count": 5 }, expectedReturn: { "FollowMouse": { @@ -211,6 +213,12 @@ gpii.tests.windows.registrySettingsHandler.enumerationTest = { "bytes": 30, "name": "Description", "type": "REG_SZ" + }, + "ZeroLength": { + "data": "", + "bytes": 2, + "name": "ZeroLength", + "type": "REG_SZ" } } }; @@ -275,13 +283,28 @@ jqUnit.test("Reading and writing single registry keys", function () { var value4 = gpii.windows.readRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "Magnification", "REG_DWORD"); jqUnit.assertDeepEq("Assert value not found after deleted", {statusCode: 404}, value4); + // Test zero-length values return an empty string (GPII-2751). + // The failure of this test also depends on the 2 bytes after the buffer being zero, so run it a few times to reduce + // the element of luck. + var tries = 500; + jqUnit.expect(tries); + for (var retry = 0; retry < tries; retry++) { + var emptyString = ""; + gpii.windows.writeRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "ZeroTest", emptyString, "REG_SZ"); + var result = gpii.windows.readRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "ZeroTest", "REG_SZ"); + jqUnit.assertEquals("Assert empty REG_SZ", 0, result.value.length); + if (result.value.length !== 0) { + // One fail is enough. + break; + } + } + // clean up after ourselves gpii.windows.deleteRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier"); // test for GPII-1108 - deletion of subkey of nonexistent parent should be permitted gpii.windows.writeRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "Magnification", undefined, "REG_DWORD"); jqUnit.assert("Deletion of subkey of nonexistent parent should be valid noop"); - }); diff --git a/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js b/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js index 1b22235ec..47ecb7df4 100644 --- a/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js +++ b/gpii/node_modules/spiSettingsHandler/src/SpiSettingsHandler.js @@ -20,7 +20,7 @@ https://github.com/GPII/universal/blob/master/LICENSE.txt var ref = require("ref"); var ffi = require("ffi"); -var fluid = require("universal"); +var fluid = require("gpii-universal"); var gpii = fluid.registerNamespace("gpii"); diff --git a/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js b/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js index e5a282828..40ccd7d35 100644 --- a/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js +++ b/gpii/node_modules/spiSettingsHandler/test/testSpiSettingsHandler.js @@ -17,7 +17,7 @@ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), jqUnit = fluid.require("node-jqunit"), gpii = fluid.registerNamespace("gpii"), $ = fluid.registerNamespace("jQuery"); diff --git a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js index ae484eae8..03b3218d4 100644 --- a/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js +++ b/gpii/node_modules/windowsMetrics/test/WindowsMetricsTests.js @@ -18,7 +18,7 @@ "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), fs = require("fs"), os = require("os"), readline = require("readline"); diff --git a/index.js b/index.js index 302bf28bf..6ea7f15a7 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ https://github.com/gpii/universal/LICENSE.txt "use strict"; -var fluid = require("universal"); +var fluid = require("gpii-universal"); fluid.module.register("gpii-windows", __dirname, require); diff --git a/package.json b/package.json index 0a9f3884e..f0b0955c5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "ref-array": "1.1.2", "edge": "6.5.1", "string-argv": "0.0.2", - "universal": "klown/universal#GPII-1939" + "gpii-universal": "klown/universal#GPII-1939" }, "devDependencies": { "grunt": "1.0.1", diff --git a/tests/AcceptanceTests.js b/tests/AcceptanceTests.js index 925fe480f..83fd97a08 100644 --- a/tests/AcceptanceTests.js +++ b/tests/AcceptanceTests.js @@ -16,7 +16,7 @@ https://github.com/gpii/universal/LICENSE.txt "use strict"; -var fluid = require("universal"), +var fluid = require("gpii-universal"), gpii = fluid.registerNamespace("gpii"); gpii.loadTestingSupport(); @@ -25,7 +25,7 @@ fluid.registerNamespace("gpii.acceptanceTesting.windows"); require("../index.js"); -var baseDir = fluid.module.resolvePath("%universal/tests/"); -var windowsFiles = fluid.require("%universal/tests/platform/index-windows.js"); +var baseDir = fluid.module.resolvePath("%gpii-universal/tests/"); +var windowsFiles = fluid.require("%gpii-universal/tests/platform/index-windows.js"); gpii.test.runSuitesWithFiltering(windowsFiles, baseDir, ["gpii.test.acceptance.testCaseHolder"]); From 7b08eca9c3c93996e0ef3d33acf3e86b42d1635b Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 16 Mar 2018 11:06:21 -0400 Subject: [PATCH 20/21] GPII-1939: Device Reporter reports all screen resolutions Merge in latest package-lock.json --- package-lock.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7cc582f8..337d2e737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2341,9 +2341,7 @@ } }, "gpii-universal": { - "version": "0.3.0-dev.20180222T171713Z.7fe2b3b", - "resolved": "https://registry.npmjs.org/gpii-universal/-/gpii-universal-0.3.0-dev.20180222T171713Z.7fe2b3b.tgz", - "integrity": "sha512-JBgQgk4xxCdnkOAWkBQvrTQk9y+kB7k1KGWE9H92ed1zo5/6JtOb7/bZpp3tbXERIJOt7L3mMQ9W0zXBPuk46Q==", + "version": "github:klown/universal#4fc120282e78eb05b43d8c4123ef2a51fcd4ea4c", "requires": { "body-parser": "1.17.2", "connect-ensure-login": "0.1.1", @@ -2356,6 +2354,7 @@ "json5": "0.5.1", "kettle": "1.7.1", "mkdirp": "0.5.1", + "ndef": "0.1.3", "nock": "9.1.0", "node-jqunit": "1.1.8", "node-uuid": "1.4.8", @@ -3683,6 +3682,11 @@ "resolved": "https://registry.npmjs.org/nan-x/-/nan-x-1.0.0.tgz", "integrity": "sha512-yw4Fhe2/UTzanQ4f0yHWkRnfTuHZFAi4GZDjXS4G+qv5BqXTqPJBbSxpa7MyyW9v4Y4ZySZQik1vcbNkhdnIOg==" }, + "ndef": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ndef/-/ndef-0.1.3.tgz", + "integrity": "sha1-geCOJEYW4wL8nDH6L9gkL3OtcJg=" + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", From ee16cea2e83659241b63ecc24ed7e5bfef779600 Mon Sep 17 00:00:00 2001 From: Joseph Scheuhammer Date: Fri, 6 Apr 2018 11:28:05 -0400 Subject: [PATCH 21/21] GPII-1939: Reference applicable version of universal. Modified to reference the version of universal just prior to incorporating Kasper's GPII-1230 code, which introduced functionality not yet implemented in the windows repository. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f446c775..102ed4eff 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "edge-js": "8.8.1", "string-argv": "0.0.2", "@pokusew/pcsclite": "0.4.18", - "gpii-universal": "klown/universal#GPII-1939" + "gpii-universal": "klown/universal#f0e0cba9423054378e809d8b9dca27a57ee9ec6c" }, "devDependencies": { "grunt": "1.0.2",