| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561 |
2x
2x
2x
2x
2x
2x
2x
2x
3x
3x
2x
9x
9x
9x
9x
9x
9x
6x
6x
6x
6x
6x
6x
2x
4x
4x
2x
7x
7x
2x
16x
16x
16x
16x
2x
13x
13x
13x
4x
13x
13x
84x
84x
84x
19x
84x
84x
12x
9x
9x
9x
9x
9x
9x
2x
5x
5x
5x
2x
13x
13x
13x
13x
13x
13x
13x
13x
2x
2x
2x
2x
2x
8x
2x
2x
38x
38x
38x
38x
4x
38x
5x
5x
4x
4x
4x
5x
38x
20x
10x
10x
10x
18x
1x
17x
38x
38x
38x
11x
5x
38x
2x
6x
6x
5x
6x
6x
5x
5x
2x
17x
17x
1x
1x
1x
17x
15x
15x
17x
17x
2x
1x
1x
2x
16x
15x
15x
15x
9x
9x
9x
9x
9x
9x
2x
9x
7x
9x
9x
6x
6x
6x
6x
16x
| /*
* 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", "gpii.metrics"],
contextAwareness: {
platform: {
checks: {
test: {
contextValue: "{gpii.contexts.test}",
gradeNames: "gpii.windowsMetrics.test"
},
windows: {
contextValue: "{gpii.contexts.windows}",
gradeNames: "gpii.windowsMetrics.windows"
}
}
}
},
listeners: {
"onDestroy.stopMetrics": "{that}.events.onStopMetrics",
"onStartMetrics.application": "{that}.startApplicationMetrics",
"onStopMetrics.application": "{that}.stopApplicationMetrics",
"onStartMetrics.input": "{that}.startInputMetrics",
"onStopMetrics.input": "{that}.stopInputMetrics"
},
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}"]
},
startInputMetrics: {
funcName: "gpii.windows.startInputMetrics",
args: ["{that}"]
},
stopInputMetrics: {
funcName: "gpii.windows.stopInputMetrics",
args: ["{that}"]
}
},
members: {
config: {
application: {
// Milliseconds to poll the active window.
precision: 5000
},
input: {
// Minimum typing session time, in milliseconds.
minSession: 30000,
// The time a session will last with no activity, in milliseconds.
sessionTimeout: 60000,
// Minimum number of keys in a typing session time.
minSessionKeys: 30000,
// Milliseconds of no input to assume inactive
inactiveTime: 300000
}
},
state: {
application: {},
input: {
keyboardHookHandle: null,
mouseHookHandle: null,
lastKeyLogged: null,
lastKeyTime: 0,
// Timestamp of typing session start.
sessionStart: null,
// Number of keys in the typing session.
keyCount: 0,
// Number of special keys
specialCount: 0,
// Mouse position
lastPos: null,
distance: 0,
lastInputTime: 0
}
}
}
});
fluid.defaults("gpii.metrics.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.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];
var isNew = oldExe !== exePath;
if (isNew) {
var data = {
exe: exePath
};
that.logMetric("app-launch", data);
Eif (!oldExe) {
runningApplications.count++;
}
runningApplications[pid] = exePath;
// It doesn't need to be constantly kept up to date, but trim it at a certain point.
Iif (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.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.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) {
Eif (that.state.application.currentProcess) {
var duration = process.hrtime(that.state.application.currentProcess.timeActivated);
var data = {
exe: that.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.state.application.currentProcess) {
that.state.application.currentProcess = {};
}
that.state.application.currentProcess.timeActivated = process.hrtime();
windows.waitForCondition(function () {
// Get the process ID that owns the active Window.
var hwnd = windows.user32.GetForegroundWindow();
Eif (hwnd) {
if (hwnd !== lastHwnd) {
lastHwnd = hwnd;
}
activePid = windows.getWindowProcessId(hwnd);
}
return activePid !== that.state.application.currentProcess.pid || !that.state.application.active;
}, {
pollDelay: that.config.application.precision
}).then(function () {
if (that.state.application.active) {
// Log how long the last window was active.
windows.logAppActivate(that);
var exePath = windows.getProcessPath(activePid);
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.
windows.checkNewApplication(that);
windows.checkActiveWindow(that);
}
});
};
/**
* Starts the input metrics.
*
* This sets up low-level keyboard and mouse hooks (WH_KEYBOARD_LL, WH_MOUSE_LL) that makes the system invoke a
* call-back whenever a key is pressed or released, or the mouse is moved/clicked.
* Hooks overview: https://msdn.microsoft.com/library/ms644959
*
* This comes with the following limitations:
* - The process needs a window-message loop. Fortunately, Electron has one so this means it will only work if running
* via gpii-app (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.startInputMetrics = function (that) {
windows.stopInputMetrics(that);
Iif (process.versions.electron) {
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], 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);
// 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, "Input metrics not available without Electron.");
}
};
/**
* Disables the key stroke metrics.
*
* Removes the low-level keyboard hook from the system, and sends the last timings to the log.
*
* @param that {Component} The gpii.windowsMetrics instance.
*/
windows.stopInputMetrics = function (that) {
var state = that.state.input;
// remove the hooks
Iif (state.keyboardHookHandle) {
windows.user32.UnhookWindowsHookEx(state.keyboardHookHandle);
}
Iif (state.mouseHookHandle) {
windows.user32.UnhookWindowsHookEx(state.mouseHookHandle);
}
state.keyboardHookHandle = null;
state.keyboardHookCallback = null;
state.mouseHookHandle = null;
state.mouseHookCallback = null;
if (state.inactivityTimer) {
clearTimeout(state.inactivityTimer);
state.inactivityTimer = null;
}
};
/**
* 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;
})());
/**
* 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 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.state.input;
var config = that.config.input;
// The time since the last key press
var keyTime = state.lastKeyTime ? timestamp - state.lastKeyTime : 0;
if (keyTime > config.sessionTimeout) {
// Only care about the time between keys in a typing session.
keyTime = 0;
}
/* "A recordable typing session would be determined only once a threshold of thirty seconds of typing has been
* reached and ending after a period of not typing for 60 seconds. (the recorded typing time for calculation would
* include the 30 seconds for threshold and exclude the 60 seconds inactivity session end threshold)"
*/
if ((state.keyCount > 1) && !keyTime) {
var duration = state.lastKeyTime - state.sessionStart;
if (duration > config.minSession && state.keyCount >= config.minSessionKeys) {
// Record the typing rate for the last typing session.
var data = {
duration: duration,
count: state.keyCount,
corrections: state.specialCount
};
// Keys per minute.
data.rate = Math.round(60000 / data.duration * data.count);
that.logMetric("typing-session", data);
}
state.keyCount = 0;
}
if (!state.keyCount) {
if (!specialKey) {
// New typing session.
state.keyCount = 1;
state.specialCount = 0;
state.sessionStart = timestamp;
}
} else if (specialKey) {
state.specialCount++;
} else {
state.keyCount++;
}
state.lastKeyTime = timestamp;
var record = {
keyTime: keyTime
};
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]];
}
}
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");
};
/**
* 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 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.inputHook = function (that, code, wparam, lparam) {
var togo;
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;
var specialKey = windows.specialKeys[eventData.vkCode];
// Ignore injected and Alt keys.
var ignoreFlags = windows.API_constants.LLKHF_INJECTED | windows.API_constants.LLKHF_ALTDOWN;
Eif ((eventData.flags & ignoreFlags) === 0) {
// If the key doesn't generate a character, then don't count it.
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.state.input.lastKeyLogged;
}
if (wanted) {
// Process in the next tick, to allow this function to return soon.
windows.recordKeyTiming(that, eventData.time, 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.
Eif ((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;
}
}
return togo;
};
|