[MM-19266] User activity monitor updates (#1061)

* user activity monitor updates

- re-work of the mechanism for determining user activity status and triggering updates that are passed to the server via the webapp
- removing system events (login/out screensaveer on/of etc.) to be re-considered for a future release

* add missing descriptions

* review tweaks

* update tests
This commit is contained in:
Dean Whillier
2019-10-09 09:00:15 -04:00
committed by GitHub
parent 695d246a67
commit e1f64f0ba0
2 changed files with 35 additions and 80 deletions

View File

@@ -15,25 +15,19 @@ export default class UserActivityMonitor extends EventEmitter {
super(); super();
this.isActive = true; this.isActive = true;
this.forceInactive = false;
this.idleTime = 0; this.idleTime = 0;
this.lastStatusUpdate = 0; this.lastSetActive = null;
this.systemIdleTimeIntervalID = -1; this.systemIdleTimeIntervalID = -1;
this.config = { this.config = {
internalUpdateFrequencyMs: 1 * 1000, // eslint-disable-line no-magic-numbers updateFrequencyMs: 1 * 1000, // eslint-disable-line no-magic-numbers
inactiveThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
statusUpdateThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers statusUpdateThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
activityTimeoutSec: 5 * 60, // eslint-disable-line no-magic-numbers
}; };
// NOTE: binding needed to prevent error; fat arrow class methods don't work in current setup
// Error: "Error: async hook stack has become corrupted (actual: #, expected: #)"
this.handleSystemGoingAway = this.handleSystemGoingAway.bind(this);
this.handleSystemComingBack = this.handleSystemComingBack.bind(this);
} }
get userIsActive() { get userIsActive() {
return this.forceInactive ? false : this.isActive; return this.isActive;
} }
get userIdleTime() { get userIdleTime() {
@@ -44,9 +38,9 @@ export default class UserActivityMonitor extends EventEmitter {
* Begin monitoring system events and idle time at defined frequency * Begin monitoring system events and idle time at defined frequency
* *
* @param {Object} config - overide internal configuration defaults * @param {Object} config - overide internal configuration defaults
* @param {nunber} config.internalUpdateFrequencyMs * @param {number} config.updateFrequencyMs - internal update clock frequency for monitoring idleTime
* @param {nunber} config.statusUpdateThresholdMs * @param {number} config.inactiveThresholdMs - the number of milliseconds that idleTime needs to reach to internally be considered inactive
* @param {nunber} config.activityTimeoutSec * @param {number} config.statusUpdateThresholdMs - minimum amount of time before sending a new status update
* @emits {error} emitted when method is called before the app is ready * @emits {error} emitted when method is called before the app is ready
* @emits {error} emitted when this method has previously been called but not subsequently stopped * @emits {error} emitted when this method has previously been called but not subsequently stopped
*/ */
@@ -63,12 +57,6 @@ export default class UserActivityMonitor extends EventEmitter {
this.config = Object.assign({}, this.config, config); this.config = Object.assign({}, this.config, config);
// NOTE: electron.powerMonitor cannot be referenced until the app is ready
electron.powerMonitor.on('suspend', this.handleSystemGoingAway);
electron.powerMonitor.on('resume', this.handleSystemComingBack);
electron.powerMonitor.on('lock-screen', this.handleSystemGoingAway);
electron.powerMonitor.on('unlock-screen', this.handleSystemComingBack);
this.systemIdleTimeIntervalID = setInterval(() => { this.systemIdleTimeIntervalID = setInterval(() => {
try { try {
electron.powerMonitor.querySystemIdleTime((idleTime) => { electron.powerMonitor.querySystemIdleTime((idleTime) => {
@@ -77,51 +65,53 @@ export default class UserActivityMonitor extends EventEmitter {
} catch (err) { } catch (err) {
console.log('Error getting system idle time:', err); console.log('Error getting system idle time:', err);
} }
}, this.config.internalUpdateFrequencyMs); }, this.config.updateFrequencyMs);
} }
/** /**
* Stop monitoring system events and idle time * Stop monitoring system events and idle time
*/ */
stopMonitoring() { stopMonitoring() {
electron.powerMonitor.off('suspend', this.handleSystemGoingAway);
electron.powerMonitor.off('resume', this.handleSystemComingBack);
electron.powerMonitor.off('lock-screen', this.handleSystemGoingAway);
electron.powerMonitor.off('unlock-screen', this.handleSystemComingBack);
clearInterval(this.systemIdleTimeIntervalID); clearInterval(this.systemIdleTimeIntervalID);
} }
/** /**
* Updates internal idle time properties and conditionally triggers updates to user activity status * Updates internal idle time and sets internal user activity state
* *
* @param {integer} idleTime * @param {integer} idleTime
* @private * @private
*/ */
updateIdleTime(idleTime) { updateIdleTime(idleTime) {
this.idleTime = idleTime; this.idleTime = idleTime;
if (idleTime * 1000 > this.config.inactiveThresholdMs) { // eslint-disable-line no-magic-numbers
if (this.idleTime > this.config.activityTimeoutSec) { this.setActivityState(false);
this.updateUserActivityStatus(false); } else {
} else if (!this.forceInactive && this.idleTime < this.config.activityTimeoutSec) { this.setActivityState(true);
this.updateUserActivityStatus(true);
} }
} }
/** /**
* Updates user activity status if changed and triggers a status update * Updates user active state and conditionally triggers a status update
* *
* @param {boolean} isActive * @param {boolean} isActive
* @param {boolean} isSystemEvent indicates whether the update was triggered by a system event (log in/out, screesaver on/off etc) * @param {boolean} isSystemEvent indicates whether the update was triggered by a system event (log in/out, screesaver on/off etc)
* @private * @private
*/ */
updateUserActivityStatus(isActive = false, isSystemEvent = false) { setActivityState(isActive = false, isSystemEvent = false) {
this.isActive = isActive;
if (isSystemEvent) {
this.sendStatusUpdate(true);
return;
}
const now = Date.now(); const now = Date.now();
if (isActive !== this.isActive) {
this.isActive = isActive; if (isActive && (this.lastSetActive == null || now - this.lastSetActive >= this.config.statusUpdateThresholdMs)) {
this.sendStatusUpdate(now, isSystemEvent); this.sendStatusUpdate(false);
} else if (now - this.lastStatusUpdate > this.config.statusUpdateThresholdMs) { this.lastSetActive = now;
this.sendStatusUpdate(now, isSystemEvent); } else if (!isActive) {
this.lastSetActive = null;
} }
} }
@@ -131,26 +121,11 @@ export default class UserActivityMonitor extends EventEmitter {
* @emits {status} emitted at regular, definable intervals providing an update on user active status and idle time * @emits {status} emitted at regular, definable intervals providing an update on user active status and idle time
* @private * @private
*/ */
sendStatusUpdate(now = Date.now(), isSystemEvent = false) { sendStatusUpdate(isSystemEvent = false) {
this.lastStatusUpdate = now;
this.emit('status', { this.emit('status', {
userIsActive: this.isActive, userIsActive: this.isActive,
idleTime: this.idleTime, idleTime: this.idleTime,
isSystemEvent, isSystemEvent,
}); });
} }
/**
* System event handlers
*
* @private
*/
handleSystemGoingAway() {
this.forceInactive = true;
this.updateUserActivityStatus(false, true);
}
handleSystemComingBack() {
this.forceInactive = false;
this.updateUserActivityStatus(true, true);
}
} }

View File

@@ -22,35 +22,15 @@ describe('UserActivityMonitor', () => {
}); });
it('should set user status to active', () => { it('should set user status to active', () => {
userActivityMonitor.updateUserActivityStatus(true); userActivityMonitor.setActivityState(true);
assert.equal(userActivityMonitor.userIsActive, true); assert.equal(userActivityMonitor.userIsActive, true);
}); });
it('should set user status to inactive', () => { it('should set user status to inactive', () => {
userActivityMonitor.updateUserActivityStatus(false); userActivityMonitor.setActivityState(false);
assert.equal(userActivityMonitor.userIsActive, false); assert.equal(userActivityMonitor.userIsActive, false);
}); });
}); });
describe('handleSystemGoingAway', () => {
it('should set user status to inactive and forceInactive to true', () => {
const userActivityMonitor = new UserActivityMonitor();
userActivityMonitor.isActive = true;
userActivityMonitor.forceInactive = false;
userActivityMonitor.handleSystemGoingAway();
assert.equal(!userActivityMonitor.userIsActive && userActivityMonitor.forceInactive, true);
});
});
describe('handleSystemComingBack', () => {
it('should set user status to active and forceInactive to false', () => {
const userActivityMonitor = new UserActivityMonitor();
userActivityMonitor.isActive = false;
userActivityMonitor.forceInactive = true;
userActivityMonitor.handleSystemComingBack();
assert.equal(userActivityMonitor.userIsActive && !userActivityMonitor.forceInactive, true);
});
});
describe('sendStatusUpdate', () => { describe('sendStatusUpdate', () => {
let userActivityMonitor; let userActivityMonitor;
@@ -62,28 +42,28 @@ describe('UserActivityMonitor', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => { userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(userIsActive && !isSystemEvent, true); assert.equal(userIsActive && !isSystemEvent, true);
}); });
userActivityMonitor.updateUserActivityStatus(true, false); userActivityMonitor.setActivityState(true, false);
}); });
it('should emit a non-system triggered status event indicating a user is inactive', () => { it('should emit a non-system triggered status event indicating a user is inactive', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => { userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(!userIsActive && !isSystemEvent, true); assert.equal(!userIsActive && !isSystemEvent, true);
}); });
userActivityMonitor.updateUserActivityStatus(false, false); userActivityMonitor.setActivityState(false, false);
}); });
it('should emit a system triggered status event indicating a user is active', () => { it('should emit a system triggered status event indicating a user is active', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => { userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(userIsActive && isSystemEvent, true); assert.equal(userIsActive && isSystemEvent, true);
}); });
userActivityMonitor.updateUserActivityStatus(true, true); userActivityMonitor.setActivityState(true, true);
}); });
it('should emit a system triggered status event indicating a user is inactive', () => { it('should emit a system triggered status event indicating a user is inactive', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => { userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(!userIsActive && isSystemEvent, true); assert.equal(!userIsActive && isSystemEvent, true);
}); });
userActivityMonitor.updateUserActivityStatus(false, true); userActivityMonitor.setActivityState(false, true);
}); });
}); });
}); });