diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 92ae7b96..c3fe6214 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -55,3 +55,4 @@ export const enum UpdateType { Archive, - Snap + Snap, + WindowsInstaller, } @@ -123 +124,38 @@ export interface IUpdateService { } + +export type Architecture = + | "arm" + | "arm64" + | "ia32" + | "loong64" + | "mips" + | "mipsel" + | "ppc" + | "ppc64" + | "riscv64" + | "s390" + | "s390x" + | "x64"; + +export type Platform = + | "aix" + | "android" + | "darwin" + | "freebsd" + | "haiku" + | "linux" + | "openbsd" + | "sunos" + | "win32" + | "cygwin" + | "netbsd"; + +export type Quality = + | "insider" + | "stable"; + +export type Target = + | "archive" + | "msi" + | "system" + | "user"; \ No newline at end of file diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index e13d1ba5..3767c907 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -17,3 +17,3 @@ import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; -import { IRequestService } from '../../request/common/request.js'; +import { asJson, IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.js'; import { StorageScope, StorageTarget } from '../../storage/common/storage.js'; @@ -21,3 +21,4 @@ import { IApplicationStorageMainService } from '../../storage/electron-main/stor import { ITelemetryService } from '../../telemetry/common/telemetry.js'; -import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; +import { Architecture, AvailableForDownload, DisablementReason, IUpdate, IUpdateService, Platform, State, StateType, Target, UpdateType } from '../common/update.js'; +import * as semver from 'semver'; @@ -30,12 +31,8 @@ export interface IUpdateURLOptions { -export function createUpdateURL(baseUpdateUrl: string, platform: string, quality: string, commit: string, options?: IUpdateURLOptions): string { - const url = new URL(`${baseUpdateUrl}/api/update/${platform}/${quality}/${commit}`); - - if (options?.background) { - url.searchParams.set('bg', 'true'); +export function createUpdateURL(productService: IProductService, quality: string, platform: Platform, architecture: Architecture, target?: Target): string { + if (target) { + return `${productService.updateUrl}/${quality}/${platform}/${architecture}/${target}/latest.json`; + } else { + return `${productService.updateUrl}/${quality}/${platform}/${architecture}/latest.json`; } - - url.searchParams.set('u', options?.internalOrg ?? 'none'); - - return url.toString(); } @@ -434,3 +431,3 @@ export abstract class AbstractUpdateService implements IUpdateService { - if (mode === 'none') { + if (mode === 'none' || mode === 'manual') { return undefined; @@ -444,18 +441,37 @@ export abstract class AbstractUpdateService implements IUpdateService { + return this._isLatestVersion(url, false) + .then((result) => { + return Promise.resolve(result ? result.lastest : result); + }) + .then(undefined, (error) => { + this.logService.error('update#isLatestVersion(): failed to check for updates'); + this.logService.error(error); + + return Promise.resolve(undefined); + }); + } + + _isLatestVersion(url: string, explicit: boolean): Promise<{lastest: boolean, update: IUpdate} | undefined> { const headers = getUpdateRequestHeaders(this.productService.version); - this.logService.trace('update#isLatestVersion() - checking update server', { url, headers }); - try { - const context = await this.requestService.request({ url, headers, callSite: 'updateService.isLatestVersion' }, token); - const statusCode = context.res.statusCode; - this.logService.trace('update#isLatestVersion() - response', { statusCode }); - // The update server replies with 204 (No Content) when no - // update is available - that's all we want to know. - return statusCode === 204; + this.logService.info('update#isLatestVersion() - checking update server', { url, headers }); - } catch (error) { - this.logService.error('update#isLatestVersion(): failed to check for updates'); - this.logService.error(error); - return undefined; - } + return this.requestService.request({ url, headers, callSite: NO_FETCH_TELEMETRY }, CancellationToken.None) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version || !update.productVersion) { + this.setState(State.Idle(UpdateType.Setup, undefined, explicit || undefined)); + + return Promise.resolve(undefined); + } + + const fetchedVersion = /\d+\.\d+\.\d+\.\d+/.test(update.productVersion) ? update.productVersion.replace(/(\d+\.\d+\.\d+)\.\d+(\-\w+)?/, '$1$2') : update.productVersion.replace(/(\d+\.\d+\.)0+(\d+)(\-\w+)?/, '$1$2$3'); + const currentVersion = this.productService.version.replace(/(\d+\.\d+\.)0+(\d+)(\-\w+)?/, '$1$2$3'); + + this.logService.info('update#isLatestVersion() - found version', fetchedVersion, currentVersion); + + const lastest = semver.compareBuild(currentVersion, fetchedVersion) >= 0; + + return Promise.resolve({ lastest, update }); + }) } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 3ddb310e..26738a64 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -16,3 +16,3 @@ import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; -import { asJson, IRequestService } from '../../request/common/request.js'; +import { asJson, IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.js'; import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js'; @@ -101,15 +101,4 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau - protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { - const assetID = this.productService.darwinUniversalAssetId ?? (process.arch === 'x64' ? 'darwin' : 'darwin-arm64'); - const url = createUpdateURL(this.productService.updateUrl!, assetID, quality, commit, options); - const headers = getUpdateRequestHeaders(this.productService.version); - try { - this.logService.trace('update#buildUpdateFeedUrl - setting feed URL for Electron autoUpdater', { url, assetID, quality, commit, headers }); - electron.autoUpdater.setFeedURL({ url, headers }); - } catch (e) { - // application is very likely not signed - this.logService.error('Failed to set update feed URL', e); - return undefined; - } - return url; + protected buildUpdateFeedUrl(quality: string, _commit: string, _options?: IUpdateURLOptions): string | undefined { + return createUpdateURL(this.productService, quality, process.platform, process.arch); } @@ -145,4 +134,30 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau - this.logService.trace('update#doCheckForUpdates - using Electron autoUpdater', { url, explicit, background }); - electron.autoUpdater.checkForUpdates(); + this.logService.info('update#doCheckForUpdates', { url, explicit, background }); + + this._isLatestVersion(url, explicit) + .then((result) => { + if(!result) { + return Promise.resolve(null); + } + + if(result.lastest) { + this.setState(State.Idle(UpdateType.Setup, undefined, explicit || undefined)); + } + else { + this.logService.info('update#doCheckForUpdates - using Electron autoUpdater'); + + electron.autoUpdater.setFeedURL({ url }); + electron.autoUpdater.checkForUpdates(); + } + + return Promise.resolve(null); + }) + .then(undefined, (error) => { + this.logService.error(error); + + // only show message when explicitly checking for updates + const message: string | undefined = explicit ? (error.message || error) : undefined; + + this.setState(State.Idle(UpdateType.Setup, message)); + }); } @@ -159,3 +174,3 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau try { - const context = await this.requestService.request({ url, headers, callSite: 'updateService.darwin.checkForUpdates' }, CancellationToken.None); + const context = await this.requestService.request({ url, headers, callSite: NO_FETCH_TELEMETRY }, CancellationToken.None); const statusCode = context.res.statusCode; diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 2be53f61..8776a7f6 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -5,3 +5,2 @@ -import { CancellationToken } from '../../../base/common/cancellation.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -13,6 +12,6 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai import { IProductService } from '../../product/common/productService.js'; -import { asJson, IRequestService } from '../../request/common/request.js'; +import { IRequestService } from '../../request/common/request.js'; import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; -import { AvailableForDownload, IUpdate, State, UpdateType } from '../common/update.js'; +import { AvailableForDownload, State, UpdateType } from '../common/update.js'; import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions } from './abstractUpdateService.js'; @@ -36,4 +35,4 @@ export class LinuxUpdateService extends AbstractUpdateService { - protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string { - return createUpdateURL(this.productService.updateUrl!, `linux-${process.arch}`, quality, commit, options); + protected buildUpdateFeedUrl(quality: string, _commit: string, _options?: IUpdateURLOptions): string { + return createUpdateURL(this.productService, quality, process.platform, process.arch); } @@ -45,2 +44,4 @@ export class LinuxUpdateService extends AbstractUpdateService { + this.setState(State.CheckingForUpdates(explicit)); + const internalOrg = this.getInternalOrg(); @@ -48,17 +49,26 @@ export class LinuxUpdateService extends AbstractUpdateService { const url = this.buildUpdateFeedUrl(this.quality, this.productService.commit!, { background, internalOrg }); - this.setState(State.CheckingForUpdates(explicit)); - this.requestService.request({ url, callSite: 'updateService.linux.checkForUpdates' }, CancellationToken.None) - .then(asJson) - .then(update => { - if (!update || !update.url || !update.version || !update.productVersion) { + this.logService.info('update#doCheckForUpdates', { url, explicit, background }); + + this._isLatestVersion(url, explicit) + .then((result) => { + if(!result) { + return Promise.resolve(null); + } + + if(result.lastest) { this.setState(State.Idle(UpdateType.Archive, undefined, explicit || undefined)); - } else { - this.setState(State.AvailableForDownload(update)); } + else { + this.setState(State.AvailableForDownload(result.update)); + } + + return Promise.resolve(null); }) - .then(undefined, err => { - this.logService.error(err); + .then(undefined, (error) => { + this.logService.error(error); + // only show message when explicitly checking for updates - const message: string | undefined = explicit ? (err.message || err) : undefined; + const message: string | undefined = explicit ? (error.message || error) : undefined; + this.setState(State.Idle(UpdateType.Archive, message)); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 222db559..0037e002 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -14,3 +14,2 @@ import { CancellationToken, CancellationTokenSource } from '../../../base/common import { memoize } from '../../../base/common/decorators.js'; -import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; @@ -32,7 +31,7 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai import { IProductService } from '../../product/common/productService.js'; -import { asJson, IRequestService } from '../../request/common/request.js'; +import { IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.js'; import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; -import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; -import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; +import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, Target, UpdateType } from '../common/update.js'; +import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions } from './abstractUpdateService.js'; @@ -50,5 +49,9 @@ function getUpdateType(): UpdateType { if (typeof _updateType === 'undefined') { - _updateType = existsSync(path.join(path.dirname(process.execPath), 'unins000.exe')) - ? UpdateType.Setup - : UpdateType.Archive; + if (existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) { + _updateType = UpdateType.Setup; + } else if (path.basename(path.normalize(path.join(process.execPath, '..', '..'))) === 'Program Files') { + _updateType = UpdateType.WindowsInstaller; + } else { + _updateType = UpdateType.Archive; + } } @@ -158,3 +161,3 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } else { - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + const fastUpdatesEnabled = getUpdateType() === UpdateType.Setup && this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); // GC for background updates in system setup happens via inno_setup since it requires @@ -178,12 +181,22 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun - protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { - let platform = `win32-${process.arch}`; - - if (getUpdateType() === UpdateType.Archive) { - platform += '-archive'; - } else if (this.productService.target === 'user') { - platform += '-user'; + protected buildUpdateFeedUrl(quality: string, _commit: string, _options?: IUpdateURLOptions): string { + let target: Target; + + switch (getUpdateType()) { + case UpdateType.Archive: + target = "archive" + break; + case UpdateType.WindowsInstaller: + target = "msi" + break; + default: + if (this.productService.target === 'user') { + target = "user" + } + else { + target = "system" + } } - return createUpdateURL(this.productService.updateUrl!, platform, quality, commit, options); + return createUpdateURL(this.productService, quality, process.platform, process.arch, target); } @@ -195,6 +208,2 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun - const internalOrg = this.getInternalOrg(); - const background = !explicit && !internalOrg; - const url = this.buildUpdateFeedUrl(this.quality, pendingCommit ?? this.productService.commit!, { background, internalOrg }); - // Only set CheckingForUpdates if we're not already in Overwriting state @@ -204,9 +213,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun - const headers = getUpdateRequestHeaders(this.productService.version); - this.requestService.request({ url, headers, callSite: 'updateService.win32.checkForUpdates' }, CancellationToken.None) - .then(asJson) - .then(update => { + const internalOrg = this.getInternalOrg(); + const background = !explicit && !internalOrg; + const url = this.buildUpdateFeedUrl(this.quality, pendingCommit ?? this.productService.commit!, { background, internalOrg }); + + this.logService.info('update#doCheckForUpdates', { url, explicit, background }); + + this._isLatestVersion(url, explicit) + .then((result) => { const updateType = getUpdateType(); - if (!update || !update.url || !update.version || !update.productVersion) { + if(!result) { // If we were checking for an overwrite update and found nothing newer, @@ -222,2 +235,9 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + const { lastest, update } = result; + + if(lastest) { + this.setState(State.Idle(updateType, undefined, explicit || undefined)); + return Promise.resolve(null); + } + if (updateType === UpdateType.Archive) { @@ -247,3 +267,3 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun - return this.requestService.request({ url: update.url, callSite: 'updateService.win32.downloadUpdate' }, CancellationToken.None) + return this.requestService.request({ url: update.url, callSite: NO_FETCH_TELEMETRY }, CancellationToken.None) .then(context => { @@ -292,8 +312,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun }) - .then(undefined, err => { - this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) }); - this.logService.error(err); + .then(undefined, (error) => { + this.logService.error(error); // only show message when explicitly checking for updates - const message: string | undefined = explicit ? (err.message || err) : undefined; + const message: string | undefined = explicit ? (error.message || error) : undefined; @@ -357,20 +376,31 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); - const child = spawn(this.availableUpdate.packagePath, - [ - '/verysilent', - '/log', - `/update="${this.availableUpdate.updateFilePath}"`, - `/progress="${progressFilePath}"`, - `/sessionend="${sessionEndFlagPath}"`, - `/cancel="${cancelFilePath}"`, - '/nocloseapplications', - '/mergetasks=runcode,!desktopicon,!quicklaunchicon' - ], - { + + let child: ChildProcess + + const type = getUpdateType(); + if (type == UpdateType.WindowsInstaller) { + child = spawn('msiexec.exe', ['/i', this.availableUpdate.packagePath], { detached: true, - stdio: ['ignore', 'ignore', 'ignore'], - windowsVerbatimArguments: true, - env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' } - } - ); + stdio: ['ignore', 'ignore', 'ignore'] + }); + } else { + child = spawn(this.availableUpdate.packagePath, + [ + '/verysilent', + '/log', + `/update="${this.availableUpdate.updateFilePath}"`, + `/progress="${progressFilePath}"`, + `/sessionend="${sessionEndFlagPath}"`, + `/cancel="${cancelFilePath}"`, + '/nocloseapplications', + '/mergetasks=runcode,!desktopicon,!quicklaunchicon' + ], + { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'], + windowsVerbatimArguments: true, + env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' } + } + ); + }