diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 080802e8..c7cbd621 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -46,6 +46,7 @@ const selectionState = require('../../shared/models/selection-state') const driveScanner = require('./modules/drive-scanner') const osDialog = require('./os/dialog') const exceptionReporter = require('./modules/exception-reporter') +const updateLock = require('./modules/update-lock') /* eslint-disable lodash/prefer-lodash-method */ @@ -284,6 +285,23 @@ app.run(($window) => { popupExists = false }).catch(exceptionReporter.report) }) + + /** + * @summary Helper fn for events + * @function + * @private + * @example + * window.addEventListener('click', extendLock) + */ + const extendLock = () => { + updateLock.extend() + } + + $window.addEventListener('click', extendLock) + $window.addEventListener('touchstart', extendLock) + + // Initial update lock acquisition + extendLock() }) app.run(($rootScope) => { diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 71ab2dd0..c86cff34 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -29,6 +29,7 @@ const errors = require('../../../shared/errors') const permissions = require('../../../shared/permissions') const windowProgress = require('../os/window-progress') const analytics = require('../modules/analytics') +const updateLock = require('./update-lock') const packageJSON = require('../../../../package.json') const selectionState = require('../../../shared/models/selection-state') @@ -269,6 +270,9 @@ exports.performWrite = (image, drives, onProgress) => { }) }) + // Clear the update lock timer to prevent longer + // flashing timing it out, and releasing the lock + updateLock.pause() ipc.server.start() }) } @@ -351,6 +355,9 @@ exports.cancel = () => { validateWriteOnSuccess: settings.get('validateWriteOnSuccess') }) + // Re-enable lock release on inactivity + updateLock.resume() + try { const [ socket ] = ipc.server.sockets ipc.server.emit(socket, 'cancel') diff --git a/lib/gui/app/modules/update-lock.js b/lib/gui/app/modules/update-lock.js new file mode 100644 index 00000000..397790f8 --- /dev/null +++ b/lib/gui/app/modules/update-lock.js @@ -0,0 +1,213 @@ +/* + * Copyright 2018 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const electron = require('electron') +const EventEmitter = require('events') +const createInactivityTimer = require('inactivity-timer') +const debug = require('debug')('etcher:update-lock') +const analytics = require('./analytics') + +/* eslint-disable no-magic-numbers, callback-return */ + +/** + * Interaction timeout in milliseconds (defaults to 5 minutes) + * @type {Number} + * @constant + */ +const INTERACTION_TIMEOUT_MS = process.env.ETCHER_INTERACTION_TIMEOUT_MS + ? parseInt(process.env.ETCHER_INTERACTION_TIMEOUT_MS, 10) + : 5 * 60 * 1000 + +/** + * Resin Update Lock + * @class + */ +class UpdateLock extends EventEmitter { + /** + * @summary Resin Update Lock + * @example + * new UpdateLock() + */ + constructor () { + super() + this.paused = false + this.on('inactive', UpdateLock.onInactive) + this.lockTimer = createInactivityTimer(INTERACTION_TIMEOUT_MS, () => { + debug('inactive') + this.emit('inactive') + }) + } + + /** + * @summary Inactivity event handler, releases the resin update lock on inactivity + * @private + * @example + * this.on('inactive', onInactive) + */ + static onInactive () { + if (process.env.ELECTRON_RESIN_UPDATE_LOCK) { + UpdateLock.check((checkError, isLocked) => { + debug('inactive-check', Boolean(checkError)) + if (checkError) { + analytics.logException(checkError) + } + if (isLocked) { + UpdateLock.release((error) => { + debug('inactive-release', Boolean(error)) + if (error) { + analytics.logException(error) + } + }) + } + }) + } + } + + /** + * @summary Acquire the update lock + * @private + * @param {Function} callback - callback(error) + * @example + * UpdateLock.acquire((error) => { + * // ... + * }) + */ + static acquire (callback) { + debug('lock') + if (process.env.ELECTRON_RESIN_UPDATE_LOCK) { + electron.ipcRenderer.once('resin-update-lock', (event, error) => { + callback(error) + }) + electron.ipcRenderer.send('resin-update-lock', 'lock') + } else { + callback(new Error('Update lock disabled')) + } + } + + /** + * @summary Release the update lock + * @private + * @param {Function} callback - callback(error) + * @example + * UpdateLock.release((error) => { + * // ... + * }) + */ + static release (callback) { + debug('unlock') + if (process.env.ELECTRON_RESIN_UPDATE_LOCK) { + electron.ipcRenderer.once('resin-update-lock', (event, error) => { + callback(error) + }) + electron.ipcRenderer.send('resin-update-lock', 'unlock') + } else { + callback(new Error('Update lock disabled')) + } + } + + /** + * @summary Check the state of the update lock + * @private + * @param {Function} callback - callback(error, isLocked) + * @example + * UpdateLock.check((error, isLocked) => { + * if (isLocked) { + * // ... + * } + * }) + */ + static check (callback) { + debug('check') + if (process.env.ELECTRON_RESIN_UPDATE_LOCK) { + electron.ipcRenderer.once('resin-update-lock', (event, error, isLocked) => { + callback(error, isLocked) + }) + electron.ipcRenderer.send('resin-update-lock', 'check') + } else { + callback(new Error('Update lock disabled')) + } + } + + /** + * @summary Extend the lock timer + * @example + * updateLock.extend() + */ + extend () { + debug('extend') + + if (this.paused) { + debug('extend:paused') + return + } + + this.lockTimer.signal() + + // When extending, check that we have the lock, + // and acquire it, if not + if (process.env.ELECTRON_RESIN_UPDATE_LOCK) { + UpdateLock.check((checkError, isLocked) => { + if (checkError) { + analytics.logException(checkError) + } + if (!isLocked) { + UpdateLock.acquire((error) => { + if (error) { + analytics.logException(error) + } + debug('extend-acquire', Boolean(error)) + }) + } + }) + } + } + + /** + * @summary Clear the lock timer + * @example + * updateLock.clearTimer() + */ + clearTimer () { + debug('clear') + this.lockTimer.clear() + } + + /** + * @summary Clear the lock timer, and pause extension, avoiding triggering until resume()d + * @example + * updateLock.pause() + */ + pause () { + debug('pause') + this.paused = true + this.clearTimer() + } + + /** + * @summary Un-pause lock extension, and restart the timer + * @example + * updateLock.resume() + */ + resume () { + debug('resume') + this.paused = false + this.extend() + } +} + +module.exports = new UpdateLock() diff --git a/lib/gui/app/pages/finish/controllers/finish.js b/lib/gui/app/pages/finish/controllers/finish.js index 72e1008e..1bf94592 100644 --- a/lib/gui/app/pages/finish/controllers/finish.js +++ b/lib/gui/app/pages/finish/controllers/finish.js @@ -21,6 +21,7 @@ const settings = require('../../../models/settings') const flashState = require('../../../../../shared/models/flash-state') const selectionState = require('../../../../../shared/models/selection-state') const analytics = require('../../../modules/analytics') +const updateLock = require('../../../modules/update-lock') const messages = require('../../../../../shared/messages') module.exports = function ($state) { @@ -57,6 +58,9 @@ module.exports = function ($state) { } selectionState.deselectAllDrives() analytics.logEvent('Restart', options) + + // Re-enable lock release on inactivity + updateLock.resume() $state.go('main') } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4ac63874..e1713218 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4363,6 +4363,16 @@ "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", "dev": true }, + "inactivity-timer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/inactivity-timer/-/inactivity-timer-1.0.0.tgz", + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + } + } + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz" diff --git a/package.json b/package.json index cdccb75f..381f3aef 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "flexboxgrid": "6.3.0", "gpt": "1.0.0", "immutable": "3.8.1", + "inactivity-timer": "1.0.0", "lodash": "4.13.1", "lzma-native": "1.5.2", "mbr": "1.1.2",