Skip to content

Commit

Permalink
Automatic background update checks.
Browse files Browse the repository at this point in the history
  • Loading branch information
arantius committed Jul 20, 2018
1 parent 1d410f2 commit b0b93bc
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 4 deletions.
3 changes: 3 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"/src/bg/options.js",
"/src/bg/user-script-detect.js",
"/src/bg/user-script-registry.js",
"/src/bg/updater.js",
"/src/bg/updater/run.js",
"/src/bg/value-store.js",
"/src/downloader.js",
"/src/parse-meta-line.js",
Expand All @@ -50,6 +52,7 @@
"/src/util/open-editor.js",
"/third-party/convert2RegExp.js",
"/third-party/MatchPattern.js",
"/third-party/compare-versions/index.js",
"/third-party/jszip/jszip.min.js",

"/src/bg/execute.run.js",
Expand Down
123 changes: 123 additions & 0 deletions src/bg/updater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Private implementation.
(function() {

const CHANGE_RATE = 1.25;
const MAX_UPDATE_IN_MS = (1000 * 60 * 60 * 24 * 7);
const MIN_UPDATE_IN_MS = (1000 * 60 * 60 * 3);


let gTimer = null;


function checkForUpdate(uuid) {
return new Promise((resolve, reject) => {
let userScript = UserScriptRegistry.scriptByUuid(uuid);
if (!userScript) {
// Uninstalled since the update check was queued.
reject('ignore');
return;
}

let windowKey = 'updateWindow.' + uuid;
chrome.storage.local.get(windowKey, windowVal => {
let abort = false;
let downloader = new UserScriptDownloader();
downloader.setScriptUrl(userScript.downloadUrl);
downloader.start(details => {
// Return false here will stop the downloader -- skipping e.g.
// @require, @icon, etc. downloads.
// `compareVersions()` returns -1 when its second argument is "larger".
let comparison = compareVersions(userScript.version, details.version);
// So we should abort if we don't get -1.
abort = comparison !== -1;
// And we should return "not stop".
return !abort;
}).then(async () => {
let window = fuzz(windowVal[windowKey] || MAX_UPDATE_IN_MS);
if (abort) {
window *= CHANGE_RATE;
} else {
window /= CHANGE_RATE;
await downloader.installFromBackground();
}
window = Math.min(window, MAX_UPDATE_IN_MS);
window = Math.max(window, MIN_UPDATE_IN_MS);

let d = {};
d[windowKey] = window;
d['updateNextAt.' + userScript.uuid] = new Date().getTime() + window;
chrome.storage.local.set(d, logUnhandledError);

if (abort) {
resolve('no new version');
} else {
resolve('updated');
}
}).catch((e) => {
reject(e);
});
});
});
}


function fuzz(num) {
return num * (Math.random() * 0.1 + 0.95);
}


/** Visible only for testing! */
window._pickNextScriptAutoUpdate = async function() {
return new Promise((resolve, reject) => {
let nextTime = null;
let nextUuid = null;

let updateNextAtKeys = [];
let userScriptIterator = UserScriptRegistry.scriptsToRunAt();
for (let userScript of userScriptIterator) {
if (!userScript.downloadUrl) continue;
updateNextAtKeys.push('updateNextAt.' + userScript.uuid);
}
if (updateNextAtKeys.length == 0) {
reject(new Error('no scripts to update'));
return;
}

let defaultCheckTime = new Date().getTime() + MIN_UPDATE_IN_MS;
chrome.storage.local.get(updateNextAtKeys, vs => {
for (let k of updateNextAtKeys) {
let uuid = k.replace('updateNextAt.', '');
let v = vs[k];

if (!nextTime || !nextUuid || v < nextTime) {
nextTime = v || defaultCheckTime;
nextUuid = uuid;
}
}

if (nextUuid) {
resolve([nextUuid, nextTime]);
} else {
reject('Could not find next script to update.');
}
});
});
};

window.scheduleNextScriptAutoUpdate = async function() {
let nextUuid, nextTime;
try {
[nextUuid, nextTime] = await _pickNextScriptAutoUpdate();
} catch (e) {
setTimeout(scheduleNextScriptAutoUpdate, fuzz(1000 * 60 * 15));
return;
}

let delay = nextTime - new Date().getTime();
gTimer = setTimeout(async (nextUuid) => {
await checkForUpdate(nextUuid);
await scheduleNextScriptAutoUpdate();
}, delay, nextUuid);
};

})();
1 change: 1 addition & 0 deletions src/bg/updater.run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// scheduleNextScriptAutoUpdate();
5 changes: 4 additions & 1 deletion src/bg/user-script-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ async function onUserScriptUninstall(message, sender, sendResponse) {
let req = store.delete(message.uuid);
db.close();

// TODO: Delete per-script values from local storage!

return new Promise((resolve, reject) => {
req.onsuccess = () => {
delete userScripts[message.uuid];
Expand Down Expand Up @@ -315,7 +317,7 @@ function scriptByUuid(scriptUuid) {
function* scriptsToRunAt(urlStr=null, includeDisabled=false) {
let url = urlStr && new URL(urlStr);

for (let uuid in userScripts) {
for (let uuid of Object.keys(userScripts)) {
let userScript = userScripts[uuid];
try {
if (!includeDisabled && !userScript.enabled) continue;
Expand All @@ -334,6 +336,7 @@ function* scriptsToRunAt(urlStr=null, includeDisabled=false) {
window.UserScriptRegistry = {
'_loadUserScripts': loadUserScripts,
'_saveUserScript': saveUserScript,
'installFromDownloader': installFromDownloader,
'scriptByUuid': scriptByUuid,
'scriptsToRunAt': scriptsToRunAt,
};
Expand Down
13 changes: 12 additions & 1 deletion src/downloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,14 @@ class Downloader {
});
}

async start() {
async installFromBackground() {
let scriptDetails = await this.scriptDetails;
let downloaderDetails = await this.details();
return UserScriptRegistry.installFromDownloader(
scriptDetails, downloaderDetails);
}

async start(detailsHandler) {
if (this._scriptContent != null) {
this.scriptDownload = new ImmediateDownload(this._scriptContent);
let scriptDetails = parseUserScript(
Expand All @@ -140,6 +147,10 @@ class Downloader {
}

let scriptDetails = await this.scriptDetails;
if (detailsHandler && !detailsHandler(scriptDetails)) {
// Abort, e.g. in case of update check with no newer version.
return;
}

if (scriptDetails.iconUrl) {
if (this._knownIconUrl == scriptDetails.iconUrl) {
Expand Down
3 changes: 2 additions & 1 deletion src/user-script-obj.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ window.RunnableUserScript = class RunnableUserScript


const editableUserScriptKeys = [
'content', 'requiresContent'];
'content', 'installTimes', 'requiresContent'];
/// A _UserScript, plus user settings, plus all requires' contents. Should
/// never be called except by `UserScriptRegistry.`
window.EditableUserScript = class EditableUserScript
Expand All @@ -244,6 +244,7 @@ window.EditableUserScript = class EditableUserScript
super(details);

this._content = null;
this._installTimes = [];
this._requiresContent = {}; // Map of download URL to content.

_loadValuesInto(this, details, editableUserScriptKeys);
Expand Down
48 changes: 48 additions & 0 deletions test/bg/updater.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';
describe('bg/updater', () => {
before(() => sinon.stub(UserScriptRegistry, 'scriptsToRunAt'));
after(() => UserScriptRegistry.scriptsToRunAt.restore());

describe('pickNextScriptAutoUpdate', () => {
it('throws with no scripts', () => {
UserScriptRegistry.scriptsToRunAt.returns([{}]);
_pickNextScriptAutoUpdate()
.catch(e => assert.equal(e.message, 'no scripts to update'));
});

it('throws with only one local script', () => {
UserScriptRegistry.scriptsToRunAt.returns([{
// no downloadUrl here!
'uuid': '6e23d732-9a17-4a23-9c3a-5203d76065d7'
}]);
_pickNextScriptAutoUpdate()
.catch(e => assert.equal(e.message, 'no scripts to update'));
});

it('returns a single script', async () => {
UserScriptRegistry.scriptsToRunAt.returns([{
'downloadUrl': 'http://example.com/anything.user.js',
'uuid': '0b6c8047-8c9b-4bbe-bf82-f4e376e284a0',
}]);
chrome.storage.local.get.callsArgWith(1, {});
let [nextUuid, _] = await _pickNextScriptAutoUpdate();
assert.equal(nextUuid, '0b6c8047-8c9b-4bbe-bf82-f4e376e284a0');
});

it('returns the proper script from among two', async () => {
UserScriptRegistry.scriptsToRunAt.returns([{
'downloadUrl': 'http://example.com/anything1.user.js',
'uuid': '7289270f-c30d-41e5-932c-560d81315565',
},{
'downloadUrl': 'http://example.com/anything2.user.js',
'uuid': '46a3926d-f64e-4800-a915-4afbe44da4fc',
}]);
chrome.storage.local.get.callsArgWith(1, {
'updateNextAt.7289270f-c30d-41e5-932c-560d81315565': 1,
'updateNextAt.46a3926d-f64e-4800-a915-4afbe44da4fc': 2,
});
let [nextUuid, _] = await _pickNextScriptAutoUpdate();
assert.equal(nextUuid, '7289270f-c30d-41e5-932c-560d81315565');
});
});
});
2 changes: 1 addition & 1 deletion test/content/backup/import.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
describe('content/import', () => {
describe('content/backup/import', () => {
function countOfMessagesNamed(messageName) {
let count = 0;
for (let call of chrome.runtime.sendMessage.getCalls()) {
Expand Down
5 changes: 5 additions & 0 deletions third-party/compare-versions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Extracted from distribution version 3.3.0:

https://github.com/omichelsen/compare-versions/releases/tag/v3.3.0

Reused under the terms of the MIT license.
75 changes: 75 additions & 0 deletions third-party/compare-versions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* global define */
(function (root, factory) {
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.compareVersions = factory();
}
}(this, function () {

var semver = /^v?(?:\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+))?(?:-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;

function indexOrEnd(str, q) {
return str.indexOf(q) === -1 ? str.length : str.indexOf(q);
}

function split(v) {
var c = v.replace(/^v/, '').replace(/\+.*$/, '');
var patchIndex = indexOrEnd(c, '-');
var arr = c.substring(0, patchIndex).split('.');
arr.push(c.substring(patchIndex + 1));
return arr;
}

function tryParse(v) {
return isNaN(Number(v)) ? v : Number(v);
}

function validate(version) {
if (typeof version !== 'string') {
throw new TypeError('Invalid argument expected string');
}
if (!semver.test(version)) {
throw new Error('Invalid argument not valid semver');
}
}

return function compareVersions(v1, v2) {
[v1, v2].forEach(validate);

var s1 = split(v1);
var s2 = split(v2);

for (var i = 0; i < Math.max(s1.length - 1, s2.length - 1); i++) {
var n1 = parseInt(s1[i] || 0, 10);
var n2 = parseInt(s2[i] || 0, 10);

if (n1 > n2) return 1;
if (n2 > n1) return -1;
}

var sp1 = s1[s1.length - 1];
var sp2 = s2[s2.length - 1];

if (sp1 && sp2) {
var p1 = sp1.split('.').map(tryParse);
var p2 = sp2.split('.').map(tryParse);

for (i = 0; i < Math.max(p1.length, p2.length); i++) {
if (p1[i] === undefined || typeof p2[i] === 'string' && typeof p1[i] === 'number') return -1;
if (p2[i] === undefined || typeof p1[i] === 'string' && typeof p2[i] === 'number') return 1;

if (p1[i] > p2[i]) return 1;
if (p2[i] > p1[i]) return -1;
}
} else if (sp1 || sp2) {
return sp1 ? -1 : 1;
}

return 0;
};

}));

0 comments on commit b0b93bc

Please sign in to comment.