2022-01-28 20:29:07 +00:00
|
|
|
const { spawn } = require('child_process');
|
2022-01-15 22:24:27 +00:00
|
|
|
const { app } = require('electron');
|
2022-03-24 12:43:18 +00:00
|
|
|
const Module = require('module');
|
2022-01-28 20:29:07 +00:00
|
|
|
const { join, resolve, basename } = require('path');
|
2022-01-15 22:24:27 +00:00
|
|
|
const { hrtime } = require('process');
|
|
|
|
|
|
|
|
const paths = require('../paths');
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
let instance;
|
|
|
|
const TASK_STATE_COMPLETE = 'Complete';
|
|
|
|
const TASK_STATE_FAILED = 'Failed';
|
|
|
|
const TASK_STATE_WAITING = 'Waiting';
|
|
|
|
const TASK_STATE_WORKING = 'Working';
|
|
|
|
|
2022-01-28 20:29:07 +00:00
|
|
|
|
2022-04-02 10:17:37 +00:00
|
|
|
class Updater extends require('events').EventEmitter {
|
2021-12-09 16:25:14 +00:00
|
|
|
constructor(options) {
|
|
|
|
super();
|
|
|
|
|
2022-03-24 13:02:48 +00:00
|
|
|
let Native;
|
2022-01-28 20:29:07 +00:00
|
|
|
try {
|
2022-03-24 13:02:48 +00:00
|
|
|
Native = options.nativeUpdaterModule ?? require(paths.getExeDir() + '/updater');
|
2022-01-28 20:29:07 +00:00
|
|
|
} catch (e) {
|
2022-04-02 10:08:44 +00:00
|
|
|
log('Updater', e); // Error when requiring
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-01-28 20:29:07 +00:00
|
|
|
if (e.code === 'MODULE_NOT_FOUND') return;
|
|
|
|
throw e;
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.committedHostVersion = null;
|
|
|
|
this.rootPath = options.root_path;
|
|
|
|
this.nextRequestId = 0;
|
|
|
|
this.requests = new Map();
|
|
|
|
this.updateEventHistory = [];
|
|
|
|
this.currentlyDownloading = {};
|
|
|
|
this.currentlyInstalling = {};
|
|
|
|
this.hasEmittedUnhandledException = false;
|
2022-03-16 10:51:06 +00:00
|
|
|
|
2022-03-24 13:02:48 +00:00
|
|
|
this.nativeUpdater = new Native.Updater({
|
2021-12-09 16:25:14 +00:00
|
|
|
response_handler: this._handleResponse.bind(this),
|
|
|
|
...options
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get valid() {
|
|
|
|
return this.nativeUpdater != null;
|
|
|
|
}
|
|
|
|
|
|
|
|
_sendRequest(detail, progressCallback = null) {
|
2022-04-02 16:16:30 +00:00
|
|
|
if (!this.valid) throw 'No native';
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
const requestId = this.nextRequestId++;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.requests.set(requestId, {
|
|
|
|
resolve,
|
|
|
|
reject,
|
|
|
|
progressCallback
|
|
|
|
});
|
2022-03-24 13:02:48 +00:00
|
|
|
|
|
|
|
this.nativeUpdater.command(JSON.stringify([ requestId, detail ]));
|
2021-12-09 16:25:14 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
_sendRequestSync(detail) {
|
2022-04-02 16:16:30 +00:00
|
|
|
if (!this.valid) throw 'No native';
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-03-24 13:02:48 +00:00
|
|
|
return this.nativeUpdater.command_blocking(JSON.stringify([ this.nextRequestId++, detail ]));
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_handleResponse(response) {
|
|
|
|
try {
|
2022-03-24 13:02:48 +00:00
|
|
|
const [ id, detail ] = JSON.parse(response);
|
2021-12-09 16:25:14 +00:00
|
|
|
const request = this.requests.get(id);
|
|
|
|
|
2022-04-02 10:08:44 +00:00
|
|
|
if (request == null) return log('Updater', id, detail); // No request handlers for id / type
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
if (detail['Error'] != null) {
|
|
|
|
const {
|
|
|
|
kind,
|
|
|
|
details,
|
|
|
|
severity
|
|
|
|
} = detail['Error'];
|
|
|
|
const e = new Error(`(${kind}) ${details}`);
|
|
|
|
|
|
|
|
if (severity === 'Fatal') {
|
2022-03-24 13:02:48 +00:00
|
|
|
if (!this.emit(kind, e)) throw e;
|
2021-12-09 16:25:14 +00:00
|
|
|
} else {
|
|
|
|
this.emit('update-error', e);
|
|
|
|
request.reject(e);
|
|
|
|
this.requests.delete(id);
|
|
|
|
}
|
|
|
|
} else if (detail === 'Ok') {
|
|
|
|
request.resolve();
|
|
|
|
this.requests.delete(id);
|
|
|
|
} else if (detail['VersionInfo'] != null) {
|
|
|
|
request.resolve(detail['VersionInfo']);
|
|
|
|
this.requests.delete(id);
|
|
|
|
} else if (detail['ManifestInfo'] != null) {
|
|
|
|
request.resolve(detail['ManifestInfo']);
|
|
|
|
this.requests.delete(id);
|
|
|
|
} else if (detail['TaskProgress'] != null) {
|
|
|
|
const msg = detail['TaskProgress'];
|
|
|
|
const progress = {
|
|
|
|
task: msg[0],
|
|
|
|
state: msg[1],
|
|
|
|
percent: msg[2],
|
|
|
|
bytesProcessed: msg[3]
|
|
|
|
};
|
|
|
|
|
|
|
|
this._recordTaskProgress(progress);
|
|
|
|
|
2022-03-24 13:02:48 +00:00
|
|
|
request.progressCallback?.(progress);
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-03-24 13:02:48 +00:00
|
|
|
if (progress.task['HostInstall'] != null && progress.state === TASK_STATE_COMPLETE) this.emit('host-updated');
|
2022-04-02 10:08:44 +00:00
|
|
|
} else log('Updater', id, detail); // Unknown response
|
2021-12-09 16:25:14 +00:00
|
|
|
} catch (e) {
|
2022-04-02 10:08:44 +00:00
|
|
|
log('Updater', e); // Error handling response
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
if (!this.hasEmittedUnhandledException) {
|
|
|
|
this.hasEmittedUnhandledException = true;
|
|
|
|
this.emit('unhandled-exception', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleSyncResponse(response) {
|
|
|
|
const detail = JSON.parse(response);
|
|
|
|
|
2022-04-02 16:16:30 +00:00
|
|
|
if (detail.Error != null) throw detail.Error;
|
2022-03-24 13:02:48 +00:00
|
|
|
else if (detail === 'Ok') return;
|
|
|
|
else if (detail.VersionInfo != null) return detail.VersionInfo;
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-04-02 10:08:44 +00:00
|
|
|
log('Updater', detail); // Unknown response
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_getHostPath() {
|
2022-03-16 10:51:06 +00:00
|
|
|
return join(this.rootPath, `app-${this.committedHostVersion.join('.')}`);
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_startCurrentVersionInner(options, versions) {
|
2022-03-16 10:51:06 +00:00
|
|
|
if (this.committedHostVersion == null) this.committedHostVersion = versions.current_host;
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-04-02 10:08:44 +00:00
|
|
|
const cur = resolve(process.execPath);
|
|
|
|
const next = resolve(join(this._getHostPath(), basename(process.execPath)));
|
2022-04-13 08:50:20 +00:00
|
|
|
|
2022-04-02 10:08:44 +00:00
|
|
|
if (next != cur && !options?.allowObsoleteHost) {
|
2022-04-04 19:55:05 +00:00
|
|
|
// Retain OpenAsar
|
|
|
|
const fs = require('original-fs');
|
|
|
|
|
|
|
|
const getAsar = (p) => join(p, '..', 'resources', 'app.asar');
|
|
|
|
const cAsar = getAsar(cur);
|
|
|
|
const nAsar = getAsar(next);
|
|
|
|
|
|
|
|
fs.copyFileSync(nAsar, nAsar + '.backup'); // Copy new app.asar to backup file (<new>/app.asar -> <new>/app.asar.backup)
|
|
|
|
fs.copyFileSync(cAsar, nAsar); // Copy old app.asar to new app.asar (<old>/app.asar -> <new>/app.asar)
|
|
|
|
|
|
|
|
app.once('will-quit', () => spawn(next, [], {
|
2022-03-24 13:02:48 +00:00
|
|
|
detached: true,
|
|
|
|
stdio: 'inherit'
|
|
|
|
}));
|
2022-03-16 10:51:06 +00:00
|
|
|
|
2022-04-02 10:08:44 +00:00
|
|
|
log('Updater', 'Restarting', next);
|
2022-03-16 10:51:06 +00:00
|
|
|
return app.quit();
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 08:54:20 +00:00
|
|
|
this._commitModulesInner(versions);
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_commitModulesInner(versions) {
|
2022-03-24 12:43:18 +00:00
|
|
|
const base = join(this._getHostPath(), 'modules');
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-04-02 10:17:37 +00:00
|
|
|
for (const m in versions.current_modules) Module.globalPaths.push(join(base, `${m}-${versions.current_modules[m]}`));
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_recordDownloadProgress(name, progress) {
|
|
|
|
const now = String(hrtime.bigint());
|
|
|
|
|
|
|
|
if (progress.state === TASK_STATE_WORKING && !this.currentlyDownloading[name]) {
|
|
|
|
this.currentlyDownloading[name] = true;
|
|
|
|
this.updateEventHistory.push({
|
|
|
|
type: 'downloading-module',
|
2022-03-24 13:02:48 +00:00
|
|
|
name,
|
|
|
|
now
|
2021-12-09 16:25:14 +00:00
|
|
|
});
|
|
|
|
} else if (progress.state === TASK_STATE_COMPLETE || progress.state === TASK_STATE_FAILED) {
|
|
|
|
this.currentlyDownloading[name] = false;
|
|
|
|
this.updateEventHistory.push({
|
|
|
|
type: 'downloaded-module',
|
2022-03-24 13:02:48 +00:00
|
|
|
name,
|
|
|
|
now,
|
2021-12-09 16:25:14 +00:00
|
|
|
succeeded: progress.state === TASK_STATE_COMPLETE,
|
|
|
|
receivedBytes: progress.bytesProcessed
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_recordInstallProgress(name, progress, newVersion, isDelta) {
|
|
|
|
const now = String(hrtime.bigint());
|
|
|
|
|
|
|
|
if (progress.state === TASK_STATE_WORKING && !this.currentlyInstalling[name]) {
|
|
|
|
this.currentlyInstalling[name] = true;
|
|
|
|
this.updateEventHistory.push({
|
|
|
|
type: 'installing-module',
|
|
|
|
name,
|
|
|
|
now,
|
2022-03-24 13:02:48 +00:00
|
|
|
newVersion
|
2021-12-09 16:25:14 +00:00
|
|
|
});
|
|
|
|
} else if (progress.state === TASK_STATE_COMPLETE || progress.state === TASK_STATE_FAILED) {
|
|
|
|
this.currentlyInstalling[name] = false;
|
|
|
|
this.updateEventHistory.push({
|
|
|
|
type: 'installed-module',
|
|
|
|
name,
|
|
|
|
now,
|
|
|
|
newVersion,
|
|
|
|
succeeded: progress.state === TASK_STATE_COMPLETE,
|
2022-03-24 13:02:48 +00:00
|
|
|
delta: isDelta
|
2021-12-09 16:25:14 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_recordTaskProgress(progress) {
|
2022-03-16 10:51:06 +00:00
|
|
|
if (progress.task.HostDownload != null) this._recordDownloadProgress('host', progress);
|
|
|
|
else if (progress.task.HostInstall != null) this._recordInstallProgress('host', progress, null, progress.task.HostInstall.from_version != null);
|
|
|
|
else if (progress.task.ModuleDownload != null) this._recordDownloadProgress(progress.task.ModuleDownload.version.module.name, progress);
|
|
|
|
else if (progress.task.ModuleInstall != null) this._recordInstallProgress(progress.task.ModuleInstall.version.module.name, progress, progress.task.ModuleInstall.version.version, progress.task.ModuleInstall.from_version != null);
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
queryCurrentVersions() {
|
|
|
|
return this._sendRequest('QueryCurrentVersions');
|
|
|
|
}
|
|
|
|
|
|
|
|
queryCurrentVersionsSync() {
|
|
|
|
return this._handleSyncResponse(this._sendRequestSync('QueryCurrentVersions'));
|
|
|
|
}
|
|
|
|
|
|
|
|
repair(progressCallback) {
|
|
|
|
return this.repairWithOptions(null, progressCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
repairWithOptions(options, progressCallback) {
|
|
|
|
return this._sendRequest({
|
|
|
|
Repair: {
|
|
|
|
options
|
|
|
|
}
|
|
|
|
}, progressCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
collectGarbage() {
|
|
|
|
return this._sendRequest('CollectGarbage');
|
|
|
|
}
|
|
|
|
|
|
|
|
setRunningManifest(manifest) {
|
|
|
|
return this._sendRequest({
|
|
|
|
SetManifests: ['Running', manifest]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
setPinnedManifestSync(manifest) {
|
|
|
|
return this._handleSyncResponse(this._sendRequestSync({
|
|
|
|
SetManifests: ['Pinned', manifest]
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
installModule(name, progressCallback) {
|
|
|
|
return this.installModuleWithOptions(name, null, progressCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
installModuleWithOptions(name, options, progressCallback) {
|
|
|
|
return this._sendRequest({
|
|
|
|
InstallModule: {
|
|
|
|
name,
|
|
|
|
options
|
|
|
|
}
|
|
|
|
}, progressCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateToLatest(progressCallback) {
|
|
|
|
return this.updateToLatestWithOptions(null, progressCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateToLatestWithOptions(options, progressCallback) {
|
|
|
|
return this._sendRequest({
|
|
|
|
UpdateToLatest: {
|
|
|
|
options
|
|
|
|
}
|
|
|
|
}, progressCallback);
|
2022-01-28 20:29:07 +00:00
|
|
|
}
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
async startCurrentVersion(options) {
|
|
|
|
const versions = await this.queryCurrentVersions();
|
|
|
|
await this.setRunningManifest(versions.last_successful_update);
|
|
|
|
|
|
|
|
this._startCurrentVersionInner(options, versions);
|
|
|
|
}
|
|
|
|
|
|
|
|
startCurrentVersionSync(options) {
|
2022-04-02 10:08:44 +00:00
|
|
|
this._startCurrentVersionInner(options, this.queryCurrentVersionsSync());
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async commitModules(versions) {
|
2022-04-02 16:16:30 +00:00
|
|
|
if (this.committedHostVersion == null) throw 'No host';
|
2021-12-09 16:25:14 +00:00
|
|
|
|
2022-03-24 12:43:18 +00:00
|
|
|
this._commitModulesInner(versions ?? await this.queryCurrentVersions());
|
2021-12-09 16:25:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
queryAndTruncateHistory() {
|
|
|
|
const history = this.updateEventHistory;
|
|
|
|
this.updateEventHistory = [];
|
|
|
|
return history;
|
|
|
|
}
|
|
|
|
|
|
|
|
getKnownFolder(name) {
|
2022-04-02 16:16:30 +00:00
|
|
|
if (!this.valid) throw 'No native';
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
return this.nativeUpdater.known_folder(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
createShortcut(options) {
|
2022-04-02 16:16:30 +00:00
|
|
|
if (!this.valid) throw 'No native';
|
2021-12-09 16:25:14 +00:00
|
|
|
|
|
|
|
return this.nativeUpdater.create_shortcut(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
Updater,
|
|
|
|
TASK_STATE_COMPLETE,
|
|
|
|
TASK_STATE_FAILED,
|
|
|
|
TASK_STATE_WAITING,
|
|
|
|
TASK_STATE_WORKING,
|
2022-03-24 13:02:48 +00:00
|
|
|
|
|
|
|
INCONSISTENT_INSTALLER_STATE_ERROR: 'InconsistentInstallerState',
|
2022-03-16 10:51:06 +00:00
|
|
|
|
|
|
|
tryInitUpdater: (buildInfo, repository_url) => {
|
|
|
|
const root_path = paths.getInstallPath();
|
|
|
|
if (root_path == null) return false;
|
|
|
|
|
|
|
|
instance = new Updater({
|
|
|
|
release_channel: buildInfo.releaseChannel,
|
2022-04-02 16:16:30 +00:00
|
|
|
platform: process.platform === 'win32' ? 'win' : 'osx',
|
2022-03-16 10:51:06 +00:00
|
|
|
repository_url,
|
|
|
|
root_path
|
|
|
|
});
|
|
|
|
|
|
|
|
return instance.valid;
|
|
|
|
},
|
|
|
|
|
|
|
|
getUpdater: () => (instance != null && instance.valid && instance) || null
|
2021-12-09 16:25:14 +00:00
|
|
|
};
|