From 86aab8a08bf128ee385bfbd6f2ca6f1140889e0c Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 12 Feb 2026 06:05:35 +0900 Subject: [PATCH] fix: customize user-agent sent via macOS updater path (#294646) --- .../electron-main/abstractUpdateService.ts | 28 +++++++++++++++-- .../electron-main/updateService.darwin.ts | 30 ++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ed54d90f383..05c4489758b 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as os from 'os'; import { IntervalTimer, timeout } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; +import { isMacintosh } from '../../../base/common/platform.js'; import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; @@ -29,6 +31,23 @@ export function createUpdateURL(baseUpdateUrl: string, platform: string, quality return url.toString(); } +/** + * Builds common headers for macOS update requests, including those issued + * via Electron's auto-updater (e.g. setFeedURL({ url, headers })) and + * manual HTTP requests that bypass the auto-updater. On macOS, this includes + * the Darwin kernel version which the update server uses for EOL detection. + */ +export function getUpdateRequestHeaders(productVersion: string): Record | undefined { + if (isMacintosh) { + const darwinVersion = os.release(); + return { + 'User-Agent': `Code/${productVersion} Darwin/${darwinVersion}` + }; + } + + return undefined; +} + export type UpdateErrorClassification = { owner: 'joaomoreno'; messageHash: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The hash of the error message.' }; @@ -288,11 +307,16 @@ export abstract class AbstractUpdateService implements IUpdateService { return 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 }, token); + const context = await this.requestService.request({ url, headers }, 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 context.res.statusCode === 204; + return statusCode === 204; } catch (error) { this.logService.error('update#isLatestVersion(): failed to check for updates'); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index fe133e702b9..e65a9823839 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -18,13 +18,14 @@ import { asJson, IRequestService } from '../../request/common/request.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AvailableForDownload, IUpdate, State, StateType, UpdateType } from '../common/update.js'; import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; -import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; +import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler { private readonly disposables = new DisposableStore(); @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } + @memoize private get onRawCheckingForUpdate(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'checking-for-update'); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } @memoize private get onRawUpdateDownloaded(): Event { @@ -68,11 +69,16 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau protected override async initialize(): Promise { await super.initialize(); this.onRawError(this.onError, this, this.disposables); + this.onRawCheckingForUpdate(this.onCheckingForUpdate, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); this.onRawUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); } + private onCheckingForUpdate(): void { + this.logService.trace('update#onCheckingForUpdate - Electron autoUpdater is checking for updates'); + } + private onError(err: string): void { this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) }); this.logService.error('UpdateService error:', err); @@ -85,8 +91,10 @@ 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 { - electron.autoUpdater.setFeedURL({ url }); + 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); @@ -126,6 +134,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } + this.logService.trace('update#doCheckForUpdates - using Electron autoUpdater', { url, explicit, background }); electron.autoUpdater.checkForUpdates(); } @@ -134,20 +143,31 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau * Used when connection is metered to show update availability without downloading. */ private async checkForUpdateNoDownload(url: string): Promise { + const headers = getUpdateRequestHeaders(this.productService.version); + this.logService.trace('update#checkForUpdateNoDownload - checking update server', { url, headers }); + try { - const update = await asJson(await this.requestService.request({ url }, CancellationToken.None)); + const context = await this.requestService.request({ url, headers }, CancellationToken.None); + const statusCode = context.res.statusCode; + this.logService.trace('update#checkForUpdateNoDownload - response', { statusCode }); + + const update = await asJson(context); if (!update || !update.url || !update.version || !update.productVersion) { + this.logService.trace('update#checkForUpdateNoDownload - no update available'); this.setState(State.Idle(UpdateType.Archive)); } else { + this.logService.trace('update#checkForUpdateNoDownload - update available', { version: update.version, productVersion: update.productVersion }); this.setState(State.AvailableForDownload(update)); } } catch (err) { - this.logService.error(err); + this.logService.error('update#checkForUpdateNoDownload - failed to check for update', err); this.setState(State.Idle(UpdateType.Archive)); } } private onUpdateAvailable(): void { + this.logService.trace('update#onUpdateAvailable - Electron autoUpdater reported update available'); + if (this.state.type !== StateType.CheckingForUpdates && this.state.type !== StateType.Overwriting) { return; } @@ -167,6 +187,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } private onUpdateNotAvailable(): void { + this.logService.trace('update#onUpdateNotAvailable - Electron autoUpdater reported no update available'); + if (this.state.type !== StateType.CheckingForUpdates) { return; }