OpenAsar/src/updater/moduleUpdater.js

435 lines
9.7 KiB
JavaScript
Raw Normal View History

2022-03-15 19:11:54 +00:00
const { join } = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp');
const yauzl = require('yauzl');
const Module = require('module');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const paths = require('../paths');
2021-12-09 16:25:14 +00:00
const request = require('./request');
2022-03-15 19:11:54 +00:00
const events = exports.events = new (require('events').EventEmitter)();
let settings,
bootstrapping,
2022-03-15 19:11:54 +00:00
skipHost, skipModule,
remote = {},
installed = {},
downloading, installing,
basePath, manifestPath, downloadPath, bootstrapPath,
hostUpdater,
baseUrl, baseQuery,
inBackground,
checking, hostAvail;
const resetTracking = () => {
const base = {
done: 0,
total: 0,
fail: 0
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
downloading = Object.assign({}, base);
installing = Object.assign({}, base);
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
exports.init = (endpoint, _settings, buildInfo) => {
log('Modules', 'Init');
2021-12-09 16:25:14 +00:00
settings = _settings;
2022-03-15 19:11:54 +00:00
skipHost = settings.get('SKIP_HOST_UPDATE');
skipModule = settings.get('SKIP_MODULE_UPDATE');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
basePath = join(paths.getUserDataVersioned(), 'modules');
manifestPath = join(basePath, 'installed.json');
downloadPath = join(basePath, 'pending');
bootstrapPath = join(paths.getResources(), 'bootstrap', 'manifest.json');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
resetTracking();
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
Module.globalPaths.push(basePath);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
// Purge pending
fs.rmSync(downloadPath, { recursive: true, force: true });
mkdirp.sync(downloadPath);
2021-12-09 16:25:14 +00:00
try {
2022-03-15 19:11:54 +00:00
installed = JSON.parse(fs.readFileSync(manifestPath));
} catch (e) {
bootstrapping = true;
2021-12-09 16:25:14 +00:00
}
2022-03-15 19:11:54 +00:00
hostUpdater = require('./hostUpdater');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
hostUpdater.on('checking-for-update', () => events.emit('checking-for-updates'));
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
hostUpdater.on('update-available', () => {
log('Modules', 'Host available');
hostAvail = true;
events.emit('update-check-finished', {
2021-12-09 16:25:14 +00:00
succeeded: true,
2022-03-15 19:11:54 +00:00
updateCount: 1,
2021-12-09 16:25:14 +00:00
manualRequired: false
});
2022-03-15 19:11:54 +00:00
events.emit('downloading-module', {
name: 'host',
current: 1,
total: 1,
foreground: !inBackground
});
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
hostUpdater.on('update-progress', progress => events.emit('downloading-module-progress', { name: 'host', progress }));
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
hostUpdater.on('update-not-available', hostPassed);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
hostUpdater.on('update-manually', (v) => {
log('Modules', 'Host manual');
checking = false;
events.emit('update-manually', {
newVersion: v
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
events.emit('update-check-finished', {
succeeded: true,
updateCount: 1,
manualRequired: true
});
});
hostUpdater.on('update-downloaded', () => {
checking = false;
events.emit('downloaded-module', {
2021-12-09 16:25:14 +00:00
name: 'host',
current: 1,
total: 1,
2022-03-15 19:11:54 +00:00
succeeded: true
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
events.emit('downloading-modules-finished', {
succeeded: 1,
failed: 0
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
});
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
hostUpdater.on('error', () => {
log('Modules', 'Host error');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
checking = false;
// die
});
2021-12-09 16:25:14 +00:00
const platform = process.platform === 'darwin' ? 'osx' : 'linux';
hostUpdater.setFeedURL.bind(hostUpdater)(`${endpoint}/updates/${buildInfo.releaseChannel}?platform=${platform}&version=${buildInfo.version}`);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
baseUrl = `${endpoint}/modules/${buildInfo.releaseChannel}`;
baseQuery = {
host_version: buildInfo.version,
platform
2022-03-15 19:11:54 +00:00
};
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const hostPassed = () => {
log('Modules', 'Host good');
if (skipModule) return events.emit('update-check-finished', {
succeeded: true,
updateCount: 0,
manualRequired: false
});
checkModules();
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const checkModules = async () => {
hostAvail = false;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const qs = {
...baseQuery,
_: Math.floor(Date.now() / 300000) // 5 min intervals
2021-12-09 16:25:14 +00:00
};
2022-03-15 19:11:54 +00:00
const url = baseUrl + '/versions.json';
log('Modules', 'Checking @', url);
let resp;
2021-12-09 16:25:14 +00:00
try {
2022-03-15 19:11:54 +00:00
resp = await request.get({ url, qs, timeout: 15000 });
checking = false;
} catch (e) {
log('Modules', 'Check failed', e);
return events.emit('update-check-finished', {
2021-12-09 16:25:14 +00:00
succeeded: false,
updateCount: 0,
manualRequired: false
});
}
2022-03-15 19:11:54 +00:00
remote = JSON.parse(resp.body);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const todo = [];
for (const name in installed) {
const inst = installed[name].installedVersion;
const rem = remote[name];
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
if (inst !== rem) {
log('Modules', 'Update:', name, '|', inst, '->', rem);
2022-03-15 19:11:54 +00:00
todo.push({ name, version: rem });
2021-12-09 16:25:14 +00:00
}
}
2022-03-15 19:11:54 +00:00
events.emit('update-check-finished', {
2021-12-09 16:25:14 +00:00
succeeded: true,
2022-03-15 19:11:54 +00:00
updateCount: todo.length,
2021-12-09 16:25:14 +00:00
manualRequired: false
});
2022-03-15 19:11:54 +00:00
if (todo.length === 0) return log('Modules', 'Nothing todo');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
for (const mod of todo) downloadModule(mod.name, mod.version);
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const downloadModule = async (name, ver) => {
downloading.total++;
events.emit('downloading-module', {
2021-12-09 16:25:14 +00:00
name,
2022-03-15 19:11:54 +00:00
current: downloading.total,
total: downloading.total,
foreground: !inBackground
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
const url = baseUrl + '/' + name + '/' + ver;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const path = join(downloadPath, name + '-' + ver + '.zip');
const stream = fs.createWriteStream(path);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
let received = 0, progress = 0;
stream.on('progress', ([recv, total]) => {
received = recv;
const nProgress = Math.min(100, Math.floor(100 * (recv / total)));
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
if (progress === nProgress) return;
progress = nProgress;
events.emit('downloading-module-progress', {
name,
progress,
recv,
total
});
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
log('Modules', 'Downloading', `${name}@${ver}`, 'from', url, 'to', path);
let success = false;
2021-12-09 16:25:14 +00:00
try {
2022-03-15 19:11:54 +00:00
const resp = await request.get({
2021-12-09 16:25:14 +00:00
url,
2022-03-15 19:11:54 +00:00
qs: baseQuery,
timeout: 15000,
2021-12-09 16:25:14 +00:00
stream
});
2022-03-15 19:11:54 +00:00
success = resp.statusCode === 200;
} catch (e) {
log('Modules', 'Fetch errored', e);
2021-12-09 16:25:14 +00:00
}
2022-03-15 19:11:54 +00:00
if (!installed[name]) installed[name] = {};
if (success) commitManifest();
else downloading.fail++;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
events.emit('downloaded-module', {
2021-12-09 16:25:14 +00:00
name: name,
2022-03-15 19:11:54 +00:00
current: downloading.total,
total: downloading.total,
succeeded: success,
receivedBytes: received
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
downloading.done++;
if (downloading.done === downloading.total) {
const succeeded = downloading.total - downloading.fail;
log('Modules', 'Done downloads', `| ${succeeded}/${downloading.total} success`);
2022-03-15 19:11:54 +00:00
events.emit('downloading-modules-finished', {
succeeded,
2022-03-15 19:11:54 +00:00
failed: downloading.fail
2021-12-09 16:25:14 +00:00
});
}
2022-03-15 19:11:54 +00:00
installModule(name, ver, path);
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const installModule = (name, ver, path) => {
const currentVer = installed[name]?.installedVersion;
installing.total++;
events.emit('installing-module', {
2021-12-09 16:25:14 +00:00
name,
2022-03-15 19:11:54 +00:00
current: installing.total,
total: installing.total,
foreground: !inBackground,
oldVersion: currentVer,
newVersion: ver
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
log('Modules', 'Installing', `${name}@${ver}`, 'from', path);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
let success = true, hasError = false;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const handleErr = (e) => {
if (hasError) return;
hasError = true;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
log('Modules', 'Failed install', `${name}@${ver}`, e);
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
success = false;
finishInstall(name, ver, success);
};
2021-12-09 16:25:14 +00:00
try {
yauzl.open(path, {}, (e, zip) => {
if (e) return handleErr(e);
const total = zip.entryCount;
let entries = 0;
zip.on('entry', () => {
entries++;
const progress = Math.min(100, Math.floor(entries / total * 100));
events.emit('installing-module-progress', {
name,
progress,
entries,
total
});
});
zip.on('error', handleErr);
zip.on('end', () => {
if (!success) return;
installed[name].installedVersion = ver;
commitManifest();
finishInstall(name, ver, success);
});
});
2022-03-15 19:11:54 +00:00
} catch (e) {
onError(e);
2021-12-09 16:25:14 +00:00
}
2022-03-15 19:11:54 +00:00
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const finishInstall = (name, ver, success) => {
if (!success) installing.fail++;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
events.emit('installed-module', {
name,
current: installing.total,
total: installing.total,
succeeded: success
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
installing.done++;
log('Modules', 'Finished', `${name}@${ver}`);
2022-03-15 19:11:54 +00:00
if (installing.done === (downloading.total || installing.done)) {
const succeeded = installing.total - installing.fail;
log('Modules', 'Done installs', `| ${succeeded}/${installing.total} success`);
2022-03-15 19:11:54 +00:00
events.emit('installing-modules-finished', {
succeeded,
2022-03-15 19:11:54 +00:00
failed: installing.fail
2021-12-09 16:25:14 +00:00
});
2022-03-15 19:11:54 +00:00
resetTracking();
bootstrapping = false;
2021-12-09 16:25:14 +00:00
}
2022-03-15 19:11:54 +00:00
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
exports.checkForUpdates = () => {
log('Modules', 'Checking');
if (checking) return;
checking = true;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
if (skipHost) {
events.emit('checking-for-updates');
hostPassed();
2021-12-09 16:25:14 +00:00
} else {
2022-03-15 19:11:54 +00:00
hostUpdater.checkForUpdates();
2021-12-09 16:25:14 +00:00
}
2022-03-15 19:11:54 +00:00
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
exports.setInBackground = () => inBackground = true;
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
exports.quitAndInstallUpdates = () => {
log('Modules', 'Relaunching');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
if (hostAvail) hostUpdater.quitAndInstall();
else {
const { app } = require('electron');
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
app.relaunch();
app.quit();
2021-12-09 16:25:14 +00:00
}
2022-03-15 19:11:54 +00:00
};
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const isInstalled = exports.isInstalled = (n, v) => installed[n] && !(v && installed[n].installedVersion !== v);
exports.getInstalled = () => ({ ...installed });
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
const commitManifest = () => fs.writeFileSync(manifestPath, JSON.stringify(installed, null, 2));
2021-12-09 16:25:14 +00:00
2022-03-15 19:11:54 +00:00
exports.install = (name, def, { version } = {}) => {
2021-12-09 16:25:14 +00:00
if (isInstalled(name, version)) {
2022-03-15 19:11:54 +00:00
if (!def) events.emit('installed-module', {
name,
current: 1,
total: 1,
succeeded: true
});
2021-12-09 16:25:14 +00:00
return;
}
2022-03-15 19:11:54 +00:00
if (def) {
installed[name] = { installedVersion: 0 };
2022-03-15 19:11:54 +00:00
return commitManifest();
2021-12-09 16:25:14 +00:00
}
downloadModule(name, version ?? remote[name] ?? 0);
2022-03-15 19:11:54 +00:00
};
exports.installPendingUpdates = () => {
2021-12-09 16:25:14 +00:00
if (bootstrapping) {
log('Modules', 'Bootstrapping...');
2021-12-09 16:25:14 +00:00
try {
for (const m in JSON.parse(fs.readFileSync(bootstrapPath))) { // Read [resources]/bootstrap/manifest.json, with "moduleName": version (always 0)
installed[m] = { installedVersion: 0 }; // Set initial
installModule(m, 0, join(bootstrapPath, m)); // Intentional invalid path
}
} catch (e) {
log('Modules', 'Bootstrap fail', e);
2021-12-09 16:25:14 +00:00
}
return;
2021-12-09 16:25:14 +00:00
}
events.emit('no-pending-updates');
2022-03-15 19:11:54 +00:00
};