From 33da0fa68fde52ff6cb84f2cca51fc59a42e69a7 Mon Sep 17 00:00:00 2001 From: Oj Date: Fri, 28 Jan 2022 20:06:36 +0000 Subject: [PATCH] [Updater > Host] Self rewrite, remove SquirrelUpdate --- src/firstRun/win32.js | 18 +-- src/updater/hostUpdater.js | 194 +++++------------------------- src/updater/squirrelUpdate.js | 218 ---------------------------------- 3 files changed, 38 insertions(+), 392 deletions(-) delete mode 100644 src/updater/squirrelUpdate.js diff --git a/src/firstRun/win32.js b/src/firstRun/win32.js index 880b93a..bf199b1 100644 --- a/src/firstRun/win32.js +++ b/src/firstRun/win32.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); const paths = require('../paths'); -const squirrel = require('../updater/squirrelUpdate'); +const windowsUtils = require('../utils/windowsUtils'); const Constants = require('../Constants'); const appPath = path.resolve(process.execPath, '..'); @@ -48,6 +48,8 @@ const updateShortcuts = (updater) => { } }; +const installProtocol = (protocol, callback) => windowsUtils.addToRegistry([['HKCU\\Software\\Classes\\' + protocol, '/ve', '/d', `URL:${protocol} Protocol`], ['HKCU\\Software\\Classes\\' + protocol, '/v', 'URL Protocol'], ['HKCU\\Software\\Classes\\' + protocol + '\\DefaultIcon', '/ve', '/d', '"' + process.execPath + '",-1'], ['HKCU\\Software\\Classes\\' + protocol + '\\shell\\open\\command', '/ve', '/d', `"${process.execPath}" --url -- "%1"`]], callback); + exports.performFirstRunTasks = (updater) => { log('FirstRun', 'Perform'); @@ -63,13 +65,13 @@ exports.performFirstRunTasks = (updater) => { log('FirstRun', 'Error updating shortcuts', e); } - squirrel.installProtocol(Constants.APP_PROTOCOL, () => { - if (shortcutSuccess) { - try { - fs.writeFileSync(firstRunCompletePath, 'true'); - } catch (e) { - log('FirstRun', 'Error writing .first-run', e); - } + installProtocol(Constants.APP_PROTOCOL, () => { + if (!shortcutSuccess) return; + + try { + fs.writeFileSync(firstRunCompletePath, 'true'); + } catch (e) { + log('FirstRun', 'Error writing .first-run', e); } }); }; diff --git a/src/updater/hostUpdater.js b/src/updater/hostUpdater.js index 833df98..f5fef31 100644 --- a/src/updater/hostUpdater.js +++ b/src/updater/hostUpdater.js @@ -1,199 +1,61 @@ -"use strict"; +const { app, autoUpdater } = require('electron'); +const events = require('events'); -Object.defineProperty(exports, "__esModule", { - value: true +const request = require('request'); + +const versionParse = (s) => s.split('.').map((x) => parseInt(x)); +const versionNewer = (a, b) => a.some((x, i) => { + const y = b[i]; + if (x == undefined) return false; + if (y == undefined) return true; + + if (x === y) return undefined; + return x > b[i]; }); -exports.default = void 0; - -var _electron = require("electron"); - -var _events = require("events"); - -var _request = _interopRequireDefault(require("./request")); - -var squirrelUpdate = _interopRequireWildcard(require("./squirrelUpdate")); - -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function versionParse(verString) { - return verString.split('.').map(i => parseInt(i)); -} - -function versionNewer(verA, verB) { - let i = 0; - - while (true) { - const a = verA[i]; - const b = verB[i]; - i++; - - if (a === undefined) { - return false; - } else { - if (b === undefined || a > b) { - return true; - } - - if (a < b) { - return false; - } - } - } -} - -class AutoUpdaterWin32 extends _events.EventEmitter { - constructor() { - super(); - this.updateUrl = null; - this.updateVersion = null; - } - - setFeedURL(updateUrl) { - this.updateUrl = updateUrl; - } - - quitAndInstall() { - if (squirrelUpdate.updateExistsSync()) { - squirrelUpdate.restart(_electron.app, this.updateVersion || _electron.app.getVersion()); - } else { - require('auto-updater').quitAndInstall(); - } - } - - downloadAndInstallUpdate(callback) { - squirrelUpdate.spawnUpdateInstall(this.updateUrl, progress => { - this.emit('update-progress', progress); - }).catch(err => callback(err)).then(() => callback()); - } - - checkForUpdates() { - if (this.updateUrl == null) { - throw new Error('Update URL is not set'); - } - - this.emit('checking-for-update'); - - if (!squirrelUpdate.updateExistsSync()) { - this.emit('update-not-available'); - return; - } - - squirrelUpdate.spawnUpdate(['--check', this.updateUrl], (error, stdout) => { - if (error != null) { - this.emit('error', error); - return; - } - - try { - // Last line of the output is JSON details about the releases - const json = stdout.trim().split('\n').pop(); - const releasesFound = JSON.parse(json).releasesToApply; - - if (releasesFound == null || releasesFound.length == 0) { - this.emit('update-not-available'); - return; - } - - const update = releasesFound.pop(); - this.emit('update-available'); - this.downloadAndInstallUpdate(error => { - if (error != null) { - this.emit('error', error); - return; - } - - this.updateVersion = update.version; - this.emit('update-downloaded', {}, update.release, update.version, new Date(), this.updateUrl, this.quitAndInstall.bind(this)); - }); - } catch (error) { - error.stdout = stdout; - this.emit('error', error); - } - }); - } - -} // todo - - -class AutoUpdaterLinux extends _events.EventEmitter { - constructor() { - super(); - this.updateUrl = null; - } +class HostLinux extends events.EventEmitter { setFeedURL(url) { this.updateUrl = url; } quitAndInstall() { - // Just restart. The splash screen will hit the update manually state and - // prompt the user to download the new package. - _electron.app.relaunch(); - - _electron.app.quit(); + app.relaunch(); + app.quit(); } async checkForUpdates() { - const currVersion = versionParse(_electron.app.getVersion()); + const current = versionParse(app.getVersion()); this.emit('checking-for-update'); try { - const response = await _request.default.get(this.updateUrl); + const [, response ] = await new Promise((res) => request.get(this.updateUrl, res)); + if (response.statusCode === 204) return this.emit('update-not-available'); - if (response.statusCode === 204) { - // you are up to date - this.emit('update-not-available'); - return; + const latest = versionParse(JSON.parse(response.body).name); + + if (versionNewer(latest, current)) { + log('HostLinux', 'Outdated'); + return this.emit('update-manually', latestVerStr); } - let latestVerStr = ''; - let latestVersion = []; - - try { - const latestMetadata = JSON.parse(response.body); - latestVerStr = latestMetadata.name; - latestVersion = versionParse(latestVerStr); - } catch (_) {} - - if (versionNewer(latestVersion, currVersion)) { - console.log('[Updates] You are out of date!'); // you need to update - - this.emit('update-manually', latestVerStr); - } else { - console.log('[Updates] You are living in the future!'); - this.emit('update-not-available'); - } + log('HostLinux', 'Not outdated'); + this.emit('update-not-available'); } catch (err) { - console.error('[Updates] Error fetching ' + this.updateUrl + ': ' + err.message); + log('HostLinux', 'Error', this.updateUrl, err.message); this.emit('error', err); } } - } -let autoUpdater; // TODO -// events: checking-for-update, update-available, update-not-available, update-manually, update-downloaded, error -// also, checkForUpdates, setFeedURL, quitAndInstall -// also, see electron.autoUpdater, and its API switch (process.platform) { case 'darwin': - autoUpdater = require('electron').autoUpdater; - break; - - case 'win32': - autoUpdater = new AutoUpdaterWin32(); + exports.default = autoUpdater; break; case 'linux': - autoUpdater = new AutoUpdaterLinux(); + exports.default = new HostLinux(); break; } -var _default = autoUpdater; -exports.default = _default; module.exports = exports.default; \ No newline at end of file diff --git a/src/updater/squirrelUpdate.js b/src/updater/squirrelUpdate.js deleted file mode 100644 index e201ba9..0000000 --- a/src/updater/squirrelUpdate.js +++ /dev/null @@ -1,218 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.spawnUpdateInstall = spawnUpdateInstall; -exports.spawnUpdate = spawnUpdate; -exports.installProtocol = installProtocol; -exports.handleStartupEvent = handleStartupEvent; -exports.updateExistsSync = updateExistsSync; -exports.restart = restart; - -var _child_process = _interopRequireDefault(require("child_process")); - -var _fs = _interopRequireDefault(require("fs")); - -var _path = _interopRequireDefault(require("path")); - -var autoStart = _interopRequireWildcard(require("../autoStart")); - -var windowsUtils = _interopRequireWildcard(require("../utils/windowsUtils")); - -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// citron note: this assumes the execPath is in the format Discord/someVersion/Discord.exe -const appFolder = _path.default.resolve(process.execPath, '..'); - -const rootFolder = _path.default.resolve(appFolder, '..'); - -const exeName = _path.default.basename(process.execPath); - -const updateExe = _path.default.join(rootFolder, 'Update.exe'); // Specialized spawn function specifically used for spawning the updater in -// update mode. Calls back with progress percentages. -// Returns Promise. - - -function spawnUpdateInstall(updateUrl, progressCallback) { - return new Promise((resolve, reject) => { - const proc = _child_process.default.spawn(updateExe, ['--update', updateUrl]); - - proc.on('error', reject); - proc.on('exit', code => { - if (code !== 0) { - return reject(new Error(`Update failed with exit code ${code}`)); - } - - return resolve(); - }); - let lastProgress = -1; - - function parseProgress() { - const lines = stdout.split(/\r?\n/); - if (lines.length === 1) return; // return the last (possibly incomplete) line to stdout for parsing again - - stdout = lines.pop(); - let currentProgress; - - for (const line of lines) { - if (!/^\d\d?$/.test(line)) continue; - const progress = Number(line); // make sure that this number is steadily increasing - - if (lastProgress > progress) continue; - currentProgress = progress; - } - - if (currentProgress == null) return; - lastProgress = currentProgress; - progressCallback(Math.min(currentProgress, 100)); - } - - let stdout = ''; - proc.stdout.on('data', chunk => { - stdout += String(chunk); - parseProgress(); - }); - }); -} // Spawn the Update.exe with the given arguments and invoke the callback when -// the command completes. - - -function spawnUpdate(args, callback) { - windowsUtils.spawn(updateExe, args, callback); -} // Create a desktop and start menu shortcut by using the command line API -// provided by Squirrel's Update.exe - - -function createShortcuts(callback, updateOnly) { - // move icon out to a more stable location, to keep shortcuts from breaking as much - const icoSrc = _path.default.join(appFolder, 'app.ico'); - - const icoDest = _path.default.join(rootFolder, 'app.ico'); - - let icoForTarget = icoDest; - - try { - const ico = _fs.default.readFileSync(icoSrc); - - _fs.default.writeFileSync(icoDest, ico); - } catch (e) { - // if we can't write there for some reason, just use the source. - icoForTarget = icoSrc; - } - - const createShortcutArgs = ['--createShortcut', exeName, '--setupIcon', icoForTarget]; - - if (updateOnly) { - createShortcutArgs.push('--updateOnly'); - } - - spawnUpdate(createShortcutArgs, callback); -} // Add a protocol registration for this application. - - -function installProtocol(protocol, callback) { - const queue = [['HKCU\\Software\\Classes\\' + protocol, '/ve', '/d', `URL:${protocol} Protocol`], ['HKCU\\Software\\Classes\\' + protocol, '/v', 'URL Protocol'], ['HKCU\\Software\\Classes\\' + protocol + '\\DefaultIcon', '/ve', '/d', '"' + process.execPath + '",-1'], ['HKCU\\Software\\Classes\\' + protocol + '\\shell\\open\\command', '/ve', '/d', `"${process.execPath}" --url -- "%1"`]]; - windowsUtils.addToRegistry(queue, callback); -} - -function terminate(app) { - app.quit(); - process.exit(0); -} // Remove the desktop and start menu shortcuts by using the command line API -// provided by Squirrel's Update.exe - - -function removeShortcuts(callback) { - spawnUpdate(['--removeShortcut', exeName], callback); -} // Update the desktop and start menu shortcuts by using the command line API -// provided by Squirrel's Update.exe - - -function updateShortcuts(callback) { - createShortcuts(callback, true); -} // Purge the protocol for this applicationstart. - - -function uninstallProtocol(protocol, callback) { - windowsUtils.spawnReg(['delete', 'HKCU\\Software\\Classes\\' + protocol, '/f'], callback); -} - -function maybeInstallNewUpdaterSeedDb() { - const installerDbSrc = _path.default.join(appFolder, 'installer.db'); - - const installerDbDest = _path.default.join(rootFolder, 'installer.db'); - - if (_fs.default.existsSync(installerDbSrc)) { - _fs.default.renameSync(installerDbSrc, installerDbDest); - } -} // Handle squirrel events denoted by --squirrel-* command line arguments. -// returns `true` if regular startup should be prevented - - -function handleStartupEvent(protocol, app, squirrelCommand) { - switch (squirrelCommand) { - case '--squirrel-install': - createShortcuts(() => { - autoStart.install(() => { - installProtocol(protocol, () => { - // Squirrel doesn't have a way to include app-level files. - // We get around this for new updater hosts, which rely on - // a seeded manifest, by bubbling the db up from the versioned-app - // directory if it exists. - maybeInstallNewUpdaterSeedDb(); - terminate(app); - }); - }); - }, false); - return true; - - case '--squirrel-updated': - updateShortcuts(() => { - autoStart.update(() => { - installProtocol(protocol, () => { - terminate(app); - }); - }); - }); - return true; - - case '--squirrel-uninstall': - removeShortcuts(() => { - autoStart.uninstall(() => { - uninstallProtocol(protocol, () => { - terminate(app); - }); - }); - }); - return true; - - case '--squirrel-obsolete': - terminate(app); - return true; - - default: - return false; - } -} // Are we using Squirrel for updates? - - -function updateExistsSync() { - return _fs.default.existsSync(updateExe); -} // Restart app as the new version - - -function restart(app, newVersion) { - app.once('will-quit', () => { - const execPath = _path.default.resolve(rootFolder, `app-${newVersion}/${exeName}`); - - _child_process.default.spawn(execPath, [], { - detached: true - }); - }); - app.quit(); -} \ No newline at end of file