import { injectable } from '@theia/core/shared/inversify'; import { UpdateInfo, CancellationToken, autoUpdater } from 'electron-updater'; import fetch, { Response } from 'node-fetch'; import { UpdateChannel } from '../../browser/arduino-preferences'; import { IDEUpdater, IDEUpdaterClient, } from '../../common/protocol/ide-updater'; const CHANGELOG_BASE_URL = 'https://downloads.arduino.cc/arduino-ide/changelog'; const IDE_DOWNLOAD_BASE_URL = 'https://downloads.arduino.cc/arduino-ide'; @injectable() export class IDEUpdaterImpl implements IDEUpdater { private isAlreadyChecked = false; private updater = autoUpdater; private cancellationToken?: CancellationToken; protected theiaFEClient?: IDEUpdaterClient; protected clients: Array = []; init(channel: UpdateChannel): void { this.updater.autoDownload = false; this.updater.channel = channel; this.updater.setFeedURL({ provider: 'generic', url: `${IDE_DOWNLOAD_BASE_URL}/${ channel === UpdateChannel.Nightly ? 'nightly' : '' }`, channel, }); this.updater.on('checking-for-update', (e) => { this.clients.forEach((c) => c.notifyCheckingForUpdate(e)); }); this.updater.on('update-available', (e) => { this.clients.forEach((c) => c.notifyUpdateAvailable(e)); }); this.updater.on('update-not-available', (e) => { this.clients.forEach((c) => c.notifyUpdateNotAvailable(e)); }); this.updater.on('download-progress', (e) => { this.clients.forEach((c) => c.notifyDownloadProgressChanged(e)); }); this.updater.on('update-downloaded', (e) => { this.clients.forEach((c) => c.notifyDownloadFinished(e)); }); this.updater.on('error', (e) => { this.clients.forEach((c) => c.notifyError(e)); }); } setClient(client: IDEUpdaterClient | undefined): void { if (client) this.clients.push(client); } async checkForUpdates(initialCheck?: boolean): Promise { if (initialCheck) { if (this.isAlreadyChecked) return Promise.resolve(); this.isAlreadyChecked = true; } const { updateInfo, cancellationToken, } = await this.updater.checkForUpdates(); this.cancellationToken = cancellationToken; if (this.updater.currentVersion.compare(updateInfo.version) === -1) { /* 'latest.txt' points to the latest changelog that has been generated by the CI, so we need to make a first GET request to get the filename of the changelog and a second GET to the actual changelog file */ try { let response: Response | null = await fetch( `${CHANGELOG_BASE_URL}/latest.txt` ); const latestChangelogFileName = response.ok ? await response.text() : null; response = latestChangelogFileName ? await fetch(`${CHANGELOG_BASE_URL}/${latestChangelogFileName}`) : null; const changelog = response?.ok ? await response?.text() : null; // We only want to see the release notes of newer versions const currentVersionIndex = changelog?.indexOf( `\r\n\r\n---\r\n\r\n## ${this.updater.currentVersion}\r\n\r\n` ); const newChangelog = currentVersionIndex && currentVersionIndex > 0 ? changelog?.slice(0, currentVersionIndex) : changelog; updateInfo.releaseNotes = newChangelog; } catch { /* if the request for the changelog fails, we'll just avoid to show it to the user, but we will still show the update info */ } return updateInfo; } } async downloadUpdate(): Promise { try { await this.updater.downloadUpdate(this.cancellationToken); } catch (e) { if (e.message === 'cancelled') return; throw e; } } stopDownload(): void { this.cancellationToken?.cancel(); } quitAndInstall(): void { this.updater.quitAndInstall(); } disconnectClient(client: IDEUpdaterClient): void { const index = this.clients.indexOf(client); if (index !== -1) { this.clients.splice(index, 1); } } dispose(): void { this.clients.forEach(this.disconnectClient.bind(this)); } }