fix: customize user-agent sent via macOS updater path (#294646)

This commit is contained in:
Robo
2026-02-12 06:05:35 +09:00
committed by GitHub
parent 3079590d75
commit 86aab8a08b
2 changed files with 52 additions and 6 deletions

View File

@@ -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<string, string> | 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');

View File

@@ -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<string> { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); }
@memoize private get onRawCheckingForUpdate(): Event<void> { return Event.fromNodeEventEmitter<void>(electron.autoUpdater, 'checking-for-update'); }
@memoize private get onRawUpdateNotAvailable(): Event<void> { return Event.fromNodeEventEmitter<void>(electron.autoUpdater, 'update-not-available'); }
@memoize private get onRawUpdateAvailable(): Event<void> { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); }
@memoize private get onRawUpdateDownloaded(): Event<IUpdate> {
@@ -68,11 +69,16 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
protected override async initialize(): Promise<void> {
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<void> {
const headers = getUpdateRequestHeaders(this.productService.version);
this.logService.trace('update#checkForUpdateNoDownload - checking update server', { url, headers });
try {
const update = await asJson<IUpdate>(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<IUpdate>(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;
}