diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index cbeb3a6..bc382cc 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -54,3 +54,4 @@ export const enum UpdateType { Archive, - Snap + Snap, + WindowsInstaller, } @@ -120 +121,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 3e5956f..b3685bb 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -18,3 +18,3 @@ import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; -import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; +import { Architecture, AvailableForDownload, DisablementReason, IUpdateService, Platform, State, StateType, Target, UpdateType } from '../common/update.js'; @@ -25,12 +25,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'); - } - - url.searchParams.set('u', options?.internalOrg ?? 'none'); - - return url.toString(); +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`; + } } @@ -315,3 +311,3 @@ export abstract class AbstractUpdateService implements IUpdateService { - if (mode === 'none') { + if (mode === 'none' || mode === 'manual') { return undefined; diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 842c676..974d411 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -22,2 +22,3 @@ import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdat import { INodeProcess } from '../../../base/common/platform.js'; +import * as semver from 'semver'; @@ -97,16 +98,5 @@ 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,3 +135,30 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.logService.trace('update#doCheckForUpdates - using Electron autoUpdater', { url, explicit, background }); - electron.autoUpdater.checkForUpdates(); + this.requestService.request({ url }, CancellationToken.None) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version || !update.productVersion) { + this.setState(State.Idle(UpdateType.Setup)); + + return Promise.resolve(null); + } + + 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') + + if(semver.compareBuild(currentVersion, fetchedVersion) >= 0) { + this.setState(State.Idle(UpdateType.Setup)); + } + else { + electron.autoUpdater.setFeedURL({ url }); + electron.autoUpdater.checkForUpdates(); + } + + return Promise.resolve(null); + }) + .then(undefined, err => { + this.logService.error(err); + // only show message when explicitly checking for updates + const message: string | undefined = explicit ? (err.message || err) : undefined; + this.setState(State.Idle(UpdateType.Setup, message)); + }); } diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 3ace29f..651b38c 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -16,2 +16,3 @@ import { AvailableForDownload, IUpdate, State, UpdateType } from '../common/upda import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions } from './abstractUpdateService.js'; +import * as semver from 'semver'; @@ -32,4 +33,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); } @@ -51,5 +52,17 @@ export class LinuxUpdateService extends AbstractUpdateService { this.setState(State.Idle(UpdateType.Archive)); - } else { + + return Promise.resolve(null); + } + + 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') + + if(semver.compareBuild(currentVersion, fetchedVersion) >= 0) { + this.setState(State.Idle(UpdateType.Archive)); + } + else { this.setState(State.AvailableForDownload(update)); } + + return Promise.resolve(null); }) diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 25535f2..7ef2f1b 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'; @@ -33,5 +32,6 @@ import { asJson, IRequestService } from '../../request/common/request.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, getUpdateRequestHeaders, IUpdateURLOptions } from './abstractUpdateService.js'; import { INodeProcess } from '../../../base/common/platform.js'; +import * as semver from 'semver'; @@ -49,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; + } } @@ -162,3 +166,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 @@ -180,12 +184,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 | undefined { + 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); } @@ -224,2 +238,10 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + 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') + + if(semver.compareBuild(currentVersion, fetchedVersion) >= 0) { + this.setState(State.Idle(updateType)); + return Promise.resolve(null); + } + if (updateType === UpdateType.Archive) { @@ -295,3 +317,2 @@ 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); @@ -359,20 +380,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' } + } + ); + }