diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index bc90a03..f8885b9 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 c943bca..1395594 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -17,4 +17,4 @@ import { ILogService } from '../../log/common/log.js'; 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 { IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.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'); +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(); } @@ -322,3 +318,3 @@ export abstract class AbstractUpdateService implements IUpdateService { - if (mode === 'none') { + if (mode === 'none' || mode === 'manual') { return undefined; @@ -336,3 +332,3 @@ export abstract class AbstractUpdateService implements IUpdateService { try { - const context = await this.requestService.request({ url, headers, callSite: 'updateService.isLatestVersion' }, token); + const context = await this.requestService.request({ url, headers, callSite: NO_FETCH_TELEMETRY }, token); const statusCode = context.res.statusCode; diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 40b38a2..323919e 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 { ITelemetryService } from '../../telemetry/common/telemetry.js'; @@ -22,2 +22,3 @@ import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdat import { INodeProcess } from '../../../base/common/platform.js'; +import * as semver from 'semver'; @@ -99,15 +100,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); } @@ -154,3 +144,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, 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(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, undefined, explicit || undefined)); + } + 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)); + }); } @@ -167,3 +184,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 0eb5d74..8ce708e 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -13,5 +13,6 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai 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 { AvailableForDownload, IUpdate, State, UpdateType } from '../common/update.js'; 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); } @@ -46,3 +47,3 @@ export class LinuxUpdateService extends AbstractUpdateService { - this.requestService.request({ url, callSite: 'updateService.linux.checkForUpdates' }, CancellationToken.None) + this.requestService.request({ url, callSite: NO_FETCH_TELEMETRY }, CancellationToken.None) .then(asJson) @@ -51,5 +52,17 @@ export class LinuxUpdateService extends AbstractUpdateService { this.setState(State.Idle(UpdateType.Archive, undefined, explicit || undefined)); - } 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, undefined, explicit || undefined)); + } + 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 d02d7c3..4e8c541 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'; @@ -31,7 +30,8 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai 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 { 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; + } } @@ -164,3 +168,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 @@ -182,12 +186,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); } @@ -209,3 +223,3 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const headers = getUpdateRequestHeaders(this.productService.version); - this.requestService.request({ url, headers, callSite: 'updateService.win32.checkForUpdates' }, CancellationToken.None) + this.requestService.request({ url, headers, callSite: NO_FETCH_TELEMETRY }, CancellationToken.None) .then(asJson) @@ -226,2 +240,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, undefined, explicit || undefined)); + return Promise.resolve(null); + } + if (updateType === UpdateType.Archive) { @@ -258,3 +280,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 => { @@ -304,3 +326,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); @@ -368,20 +389,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' } + } + ); + }