From eb738e841a1be547a114c67c93bfce4a7549551b Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Tue, 3 Dec 2024 13:13:00 -0500 Subject: [PATCH] [menu-bar][electron] Implement linux support for auto updater (#229) * [menu-bar][electron] Implement linux autoupdater * Use pkexec to run sudo install * Autorestart app * Update create install command * Add changelog entry --- CHANGELOG.md | 1 + .../modules/auto-updater/electron/Updater.ts | 4 - .../auto-updater/electron/platform/Linux.ts | 118 +++++++----------- 3 files changed, 48 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109e7557..15b802bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 🎉 New features - Allow users to manually launch updates in Expo Go. ([#226](https://github.com/expo/orbit/pull/226) by [@gabrieldonadel](https://github.com/gabrieldonadel)) +- [Linux] Add support for auto updates. ([#229](https://github.com/expo/orbit/pull/229) by [@gabrieldonadel](https://github.com/gabrieldonadel)) ### 🐛 Bug fixes diff --git a/apps/menu-bar/modules/auto-updater/electron/Updater.ts b/apps/menu-bar/modules/auto-updater/electron/Updater.ts index c9daa62f..8ab3ac93 100644 --- a/apps/menu-bar/modules/auto-updater/electron/Updater.ts +++ b/apps/menu-bar/modules/auto-updater/electron/Updater.ts @@ -122,10 +122,6 @@ export default class Updater extends EventEmitter { checkForUpdates({ silent }: { silent?: boolean } = {}): this { this.silent = silent; const opt = this.options; - if (process.platform === 'linux') { - this.emit('error', 'Updates are not available on Linux yet'); - return this; - } if (!opt.url) { this.emit('error', 'You must set url before calling checkForUpdates()'); diff --git a/apps/menu-bar/modules/auto-updater/electron/platform/Linux.ts b/apps/menu-bar/modules/auto-updater/electron/platform/Linux.ts index 0912783d..509ecbc6 100644 --- a/apps/menu-bar/modules/auto-updater/electron/platform/Linux.ts +++ b/apps/menu-bar/modules/auto-updater/electron/platform/Linux.ts @@ -27,61 +27,73 @@ export default class Linux extends Platform { downloadUpdate(buildInfo: BuildInfo) { this.downloadUpdateFile(buildInfo) .then(() => { - this.logger.info(`New version has been downloaded from ${buildInfo.url} `); + this.logger.info(`New version has been downloaded from ${buildInfo.url}`); this.emit('update-downloaded', this.meta); + + this.quitAndInstall(); }) .catch((e) => this.emit('error', e)); } - /** - * @param {boolean} restartRequired - */ - quitAndInstall(restartRequired = true) { + createInstallCommand(updatePath: string) { + const fileExtension = path.extname(updatePath); + switch (fileExtension) { + case '.deb': + return ['dpkg', '-i', updatePath]; + case '.rpm': + return ['rpm', '-i', '--force', updatePath]; + default: + throw new Error('Unsupported package format. Only .deb and .rpm are supported.'); + } + } + + quitAndInstall() { if (!this.lastUpdatePath) { return; } - // @ts-ignore - app.off('will-quit', this.quitAndInstall); - - const updateScript = ` - if [ "\${RESTART_REQUIRED}" = 'true' ]; then - cp -f "\${UPDATE_FILE}" "\${APP_IMAGE}" - (exec "\${APP_IMAGE}") & disown $! - else - (sleep 2 && cp -f "\${UPDATE_FILE}" "\${APP_IMAGE}") & disown $! - fi - kill "\${OLD_PID}" $(ps -h --ppid "\${OLD_PID}" -o pid) - rm "\${UPDATE_FILE}" - `; - - const proc = spawn('/bin/bash', ['-c', updateScript], { + const installCommand = this.createInstallCommand(this.lastUpdatePath); + + if (!installCommand) { + throw new Error('Unsupported package format. Only .deb and .rpm are supported.'); + } + + const proc = spawn('pkexec', installCommand, { detached: true, - stdio: 'ignore', - env: { - ...process.env, - APP_IMAGE: this.getAppImagePath(), - // @ts-ignore - OLD_PID: process.pid, - RESTART_REQUIRED: String(restartRequired), - UPDATE_FILE: this.lastUpdatePath, - }, + stdio: 'inherit', }); - // @ts-ignore - proc.unref(); - if (restartRequired === true) { + proc.on('exit', (code) => { + if (code !== 0) { + this.emit('error', `Installation process failed with code ${code}`); + return; + } + this.logger.info('Update installed successfully.'); + + // Relaunch the app + const appPath = process.argv[0]; // Path to the current executable + const appArgs = process.argv.slice(1); // Current arguments + + this.logger.info(`Relaunching app from path: ${appPath} with args: ${appArgs.join(' ')}`); + spawn(appPath, appArgs, { + detached: true, + stdio: 'ignore', + }).unref(); + quit(); process.exit(); - } + }); + + proc.unref(); } async downloadUpdateFile(buildInfo: BuildInfo) { - this.lastUpdatePath = this.getUpdatePath(buildInfo.sha256 || uuidv4()); + const fileExtension = buildInfo.url.endsWith('.deb') ? '.deb' : '.rpm'; + const fileName = `${app.getName()}-${uuidv4()}${fileExtension}`; + this.lastUpdatePath = path.join(os.tmpdir(), fileName); if (!fs.existsSync(this.lastUpdatePath)) { await this.httpClient.downloadFile(buildInfo.url, this.lastUpdatePath); - await setExecFlag(this.lastUpdatePath); } if (buildInfo.sha256) { @@ -93,27 +105,9 @@ export default class Linux extends Platform { } } - // @ts-ignore - app.on('will-quit', this.quitAndInstall); - return this.lastUpdatePath; } - getAppImagePath() { - const appImagePath = process.env.APPIMAGE; - - if (!appImagePath) { - throw new Error('It seems that the app is not in AppImage format'); - } - - return appImagePath; - } - - getUpdatePath(id: string) { - const fileName = `${app.getName()}-${id}.AppImage`; - return path.join(os.tmpdir(), fileName); - } - async checkHash(hash: string, filePath: string) { const fileHash = await calcSha256Hash(filePath); if (fileHash !== hash) { @@ -121,21 +115,3 @@ export default class Linux extends Platform { } } } - -async function setExecFlag(filePath: string) { - return new Promise((resolve, reject) => { - fs.access(filePath, fs.constants.X_OK, (err) => { - if (!err) { - return resolve(filePath); - } - - fs.chmod(filePath, '0755', (e) => { - if (e) { - reject(new Error(`Cannot chmod of ${filePath}`)); - } else { - resolve(filePath); - } - }); - }); - }); -}