From fdba6d433a06fc588a2813d0c5ec54c3bbedcc8d Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 20 Aug 2026 20:53:23 -0700 Subject: [PATCH 01/11] meteredConnection: use native platform monitor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/.moduleignore | 7 ++ eslint.config.js | 1 + package-lock.json | 24 ++++ package.json | 2 + src/vs/code/electron-main/app.ts | 2 +- .../node/nativeModules.integrationTest.ts | 5 + .../browser/meteredConnectionService.ts | 40 ++++++- .../common/meteredConnection.ts | 61 ++-------- .../common/meteredConnectionIpc.ts | 18 +-- .../meteredConnectionService.ts | 43 ++----- .../electron-main/meteredConnectionChannel.ts | 3 - .../meteredConnectionMainService.ts | 91 ++++++++++++-- .../meteredConnectionService.test.ts | 65 +++++++--- .../meteredConnectionMainService.test.ts | 113 +++++++++++++++++- 14 files changed, 346 insertions(+), 129 deletions(-) diff --git a/build/.moduleignore b/build/.moduleignore index fec2fbe2ba65ad..c650212ad62bd4 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -155,6 +155,13 @@ vsda/** !vsda/rust/web/** !vsda/rust/bundler/** +@vscode/metered/build/** +@vscode/metered/src/** +@vscode/metered/binding.gyp +@vscode/metered/README.md +@vscode/metered/index.d.ts +!@vscode/metered/build/Release/vscode-metered.node + @vscode/policy-watcher/build/** @vscode/policy-watcher/.husky/** @vscode/policy-watcher/src/** diff --git a/eslint.config.js b/eslint.config.js index b9221655462a09..d72bb73bba562b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1543,6 +1543,7 @@ export default defineConfig( '@vscode/vscode-languagedetection', '@vscode/ripgrep-universal', '@vscode/iconv-lite-umd', + '@vscode/metered', '@vscode/native-watchdog', '@vscode/policy-watcher', '@vscode/proxy-agent', diff --git a/package-lock.json b/package-lock.json index 2c53ca845ce75f..c5e9841a232a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@vscode/diff": "0.0.2-7", "@vscode/fs-copyfile": "2.0.0", "@vscode/iconv-lite-umd": "0.7.1", + "@vscode/metered": "^0.1.0", "@vscode/native-watchdog": "^1.4.6", "@vscode/os-proxy-resolver": "^0.3.0", "@vscode/policy-watcher": "^1.4.0", @@ -4625,6 +4626,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@vscode/metered": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@vscode/metered/-/metered-0.1.0.tgz", + "integrity": "sha512-UOyIf3bdQTPEaXuqPVP617p5V90hrdS7eWQU+GoQ4bcCIv9MwZkBqWIoq7qucGvTX7YpIDwhGKvZjtJFbgbMRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/metered/node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/@vscode/native-watchdog": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@vscode/native-watchdog/-/native-watchdog-1.4.6.tgz", diff --git a/package.json b/package.json index 3c518289b1eb2b..35799586238aed 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@vscode/diff": "0.0.2-7", "@vscode/fs-copyfile": "2.0.0", "@vscode/iconv-lite-umd": "0.7.1", + "@vscode/metered": "^0.1.0", "@vscode/native-watchdog": "^1.4.6", "@vscode/os-proxy-resolver": "^0.3.0", "@vscode/policy-watcher": "^1.4.0", @@ -313,6 +314,7 @@ "@vscode/native-watchdog@1.4.6": true, "@vscode/ripgrep@1.17.1": true, "@vscode/deviceid@0.1.5": true, + "@vscode/metered@0.1.0": true, "@vscode/policy-watcher@1.4.0": true, "@vscode/spdlog@0.15.8": true, "@vscode/sqlite3@5.1.12-vscode": true, diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 8176a5b4bb2104..f6488c3ce535ed 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1212,7 +1212,7 @@ export class CodeApplication extends Disposable { services.set(IGlobalKeybindingsMainService, new SyncDescriptor(GlobalKeybindingsMainService, [globalShortcut])); // Metered Connection - const meteredConnectionService = new MeteredConnectionMainService(this.configurationService); + const meteredConnectionService = new MeteredConnectionMainService(undefined, this.configurationService, this.logService); services.set(IMeteredConnectionService, meteredConnectionService); // Web Contents Extractor diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 4b0ef7f29aa046..0c156bacfad9fd 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -65,6 +65,11 @@ flakySuite('Native Modules (all platforms)', () => { assert.ok(typeof watcher.createWatcher === 'function', testErrorMessage('@vscode/policy-watcher')); }); + test('@vscode/metered', async () => { + const metered = await import('@vscode/metered'); + assert.ok(typeof metered.createMonitor === 'function', testErrorMessage('@vscode/metered')); + }); + test('node-pty', async () => { const nodePty = await import('node-pty'); assert.ok(typeof nodePty.spawn === 'function', testErrorMessage('node-pty')); diff --git a/src/vs/platform/meteredConnection/browser/meteredConnectionService.ts b/src/vs/platform/meteredConnection/browser/meteredConnectionService.ts index 19f9c84f8a90da..9734e222c86310 100644 --- a/src/vs/platform/meteredConnection/browser/meteredConnectionService.ts +++ b/src/vs/platform/meteredConnection/browser/meteredConnectionService.ts @@ -6,7 +6,43 @@ import { toDisposable } from '../../../base/common/lifecycle.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; -import { AbstractMeteredConnectionService, getIsBrowserConnectionMetered, IMeteredConnectionService, NavigatorWithConnection } from '../common/meteredConnection.js'; +import { AbstractMeteredConnectionService, IMeteredConnectionService } from '../common/meteredConnection.js'; + +/** + * Browser Network Information API properties used for metered detection. + * See https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API + */ +interface NetworkInformation { + saveData?: boolean; + metered?: boolean; + effectiveType?: 'slow-2g' | '2g' | '3g' | '4g'; + addEventListener(type: 'change', listener: () => void): void; + removeEventListener(type: 'change', listener: () => void): void; +} + +/** + * Extends Navigator with the optional browser Network Information API. + */ +interface NavigatorWithConnection { + readonly connection?: NetworkInformation; +} + +/** + * Returns whether the browser Network Information API indicates a metered connection. + */ +function getIsBrowserConnectionMetered(): boolean { + const connection = (navigator as NavigatorWithConnection).connection; + if (!connection) { + return false; + } + + if (connection.saveData || connection.metered) { + return true; + } + + const effectiveType = connection.effectiveType; + return effectiveType === '2g' || effectiveType === 'slow-2g'; +} /** * Browser implementation of the metered connection service. @@ -18,7 +54,7 @@ export class MeteredConnectionService extends AbstractMeteredConnectionService { const connection = (navigator as NavigatorWithConnection).connection; if (connection) { - const onChange = () => this.setIsBrowserConnectionMetered(getIsBrowserConnectionMetered()); + const onChange = () => this.setIsUnderlyingConnectionMetered(getIsBrowserConnectionMetered()); connection.addEventListener('change', onChange); this._register(toDisposable(() => connection.removeEventListener('change', onChange))); } diff --git a/src/vs/platform/meteredConnection/common/meteredConnection.ts b/src/vs/platform/meteredConnection/common/meteredConnection.ts index 9448920e163844..b97c48738c72a0 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnection.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnection.ts @@ -35,45 +35,8 @@ export interface IMeteredConnectionService { } export const METERED_CONNECTION_SETTING_KEY = 'network.meteredConnection'; - export type MeteredConnectionSettingValue = 'on' | 'off' | 'auto'; -/** - * Network Information API - * See https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API - */ -export interface NetworkInformation { - saveData?: boolean; - metered?: boolean; - effectiveType?: 'slow-2g' | '2g' | '3g' | '4g'; - addEventListener(type: 'change', listener: () => void): void; - removeEventListener(type: 'change', listener: () => void): void; -} - -/** - * Extended Navigator interface for Network Information API - */ -export interface NavigatorWithConnection { - readonly connection?: NetworkInformation; -} - -/** - * Check if the current network connection is metered according to the Network Information API. - */ -export function getIsBrowserConnectionMetered() { - const connection = (navigator as NavigatorWithConnection).connection; - if (!connection) { - return false; - } - - if (connection.saveData || connection.metered) { - return true; - } - - const effectiveType = connection.effectiveType; - return effectiveType === '2g' || effectiveType === 'slow-2g'; -} - /** * Abstract base class for metered connection services. */ @@ -84,15 +47,15 @@ export abstract class AbstractMeteredConnectionService extends Disposable implem public readonly onDidChangeIsConnectionMetered = this._onDidChangeIsConnectionMetered.event; private _isConnectionMetered: boolean; - private _isBrowserConnectionMetered: boolean; + private _isUnderlyingConnectionMetered: boolean; private _meteredConnectionSetting: MeteredConnectionSettingValue; - constructor(configurationService: IConfigurationService, isBrowserConnectionMetered: boolean) { + constructor(configurationService: IConfigurationService, isUnderlyingConnectionMetered: boolean) { super(); - this._isBrowserConnectionMetered = isBrowserConnectionMetered; + this._isUnderlyingConnectionMetered = isUnderlyingConnectionMetered; this._meteredConnectionSetting = configurationService.getValue(METERED_CONNECTION_SETTING_KEY); - this._isConnectionMetered = this._meteredConnectionSetting === 'on' || (this._meteredConnectionSetting !== 'off' && this._isBrowserConnectionMetered); + this._isConnectionMetered = this._meteredConnectionSetting === 'on' || (this._meteredConnectionSetting !== 'off' && this._isUnderlyingConnectionMetered); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(METERED_CONNECTION_SETTING_KEY)) { @@ -109,23 +72,23 @@ export abstract class AbstractMeteredConnectionService extends Disposable implem return this._isConnectionMetered; } - protected get isBrowserConnectionMetered(): boolean { - return this._isBrowserConnectionMetered; + protected get isUnderlyingConnectionMetered(): boolean { + return this._isUnderlyingConnectionMetered; } - public setIsBrowserConnectionMetered(value: boolean) { - if (value !== this._isBrowserConnectionMetered) { - this._isBrowserConnectionMetered = value; - this.onChangeBrowserConnection(); + protected setIsUnderlyingConnectionMetered(value: boolean) { + if (value !== this._isUnderlyingConnectionMetered) { + this._isUnderlyingConnectionMetered = value; + this.onChangeUnderlyingConnection(); } } - protected onChangeBrowserConnection() { + protected onChangeUnderlyingConnection() { this.onUpdated(); } protected onUpdated() { - const value = this._meteredConnectionSetting === 'on' || (this._meteredConnectionSetting !== 'off' && this._isBrowserConnectionMetered); + const value = this._meteredConnectionSetting === 'on' || (this._meteredConnectionSetting !== 'off' && this._isUnderlyingConnectionMetered); if (value !== this._isConnectionMetered) { this._isConnectionMetered = value; this.onChangeIsConnectionMetered(); diff --git a/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts b/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts index 5cfbeae576ae84..5c4ea0d164acdc 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { onUnexpectedError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; @@ -16,7 +17,6 @@ export const METERED_CONNECTION_CHANNEL = 'meteredConnection'; export enum MeteredConnectionCommand { OnDidChangeIsConnectionMetered = 'OnDidChangeIsConnectionMetered', IsConnectionMetered = 'IsConnectionMetered', - SetIsBrowserConnectionMetered = 'SetIsBrowserConnectionMetered', } /** @@ -36,18 +36,20 @@ export class MeteredConnectionChannelClient extends Disposable implements IMeter constructor(channel: IChannel) { super(); - channel.call(MeteredConnectionCommand.IsConnectionMetered).then(value => { - this._isConnectionMetered = value; - if (value) { - this._onDidChangeIsConnectionMetered.fire(value); - } - }); - + let receivedEvent = false; this._register(channel.listen(MeteredConnectionCommand.OnDidChangeIsConnectionMetered)(value => { + receivedEvent = true; if (this._isConnectionMetered !== value) { this._isConnectionMetered = value; this._onDidChangeIsConnectionMetered.fire(value); } })); + + channel.call(MeteredConnectionCommand.IsConnectionMetered).then(value => { + if (!receivedEvent && this._isConnectionMetered !== value) { + this._isConnectionMetered = value; + this._onDidChangeIsConnectionMetered.fire(value); + } + }, onUnexpectedError); } } diff --git a/src/vs/platform/meteredConnection/electron-browser/meteredConnectionService.ts b/src/vs/platform/meteredConnection/electron-browser/meteredConnectionService.ts index 728ce40afccf40..6e7d32819709d7 100644 --- a/src/vs/platform/meteredConnection/electron-browser/meteredConnectionService.ts +++ b/src/vs/platform/meteredConnection/electron-browser/meteredConnectionService.ts @@ -3,46 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { toDisposable } from '../../../base/common/lifecycle.js'; -import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { IConfigurationService } from '../../configuration/common/configuration.js'; -import { SyncDescriptor } from '../../instantiation/common/descriptors.js'; -import { registerSingleton } from '../../instantiation/common/extensions.js'; +import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; -import { AbstractMeteredConnectionService, getIsBrowserConnectionMetered, IMeteredConnectionService, NavigatorWithConnection } from '../common/meteredConnection.js'; -import { METERED_CONNECTION_CHANNEL, MeteredConnectionCommand } from '../common/meteredConnectionIpc.js'; +import { IMeteredConnectionService } from '../common/meteredConnection.js'; +import { METERED_CONNECTION_CHANNEL, MeteredConnectionChannelClient } from '../common/meteredConnectionIpc.js'; /** * Electron-browser implementation of the metered connection service. - * This implementation monitors navigator.connection and reports changes to the main process via IPC channel. + * The native state and user override are owned by the main process. */ -export class NativeMeteredConnectionService extends AbstractMeteredConnectionService { - private readonly _channel: IChannel; - - constructor( - private readonly connectionMeteredDetector: () => boolean, - @IConfigurationService configurationService: IConfigurationService, - @IMainProcessService mainProcessService: IMainProcessService - ) { - super(configurationService, connectionMeteredDetector()); - this._channel = mainProcessService.getChannel(METERED_CONNECTION_CHANNEL); - void this._channel.call(MeteredConnectionCommand.SetIsBrowserConnectionMetered, this.isBrowserConnectionMetered); - - const connection = (navigator as NavigatorWithConnection).connection; - if (connection) { - const onChange = () => this.setIsBrowserConnectionMetered(this.connectionMeteredDetector()); - connection.addEventListener('change', onChange); - this._register(toDisposable(() => connection.removeEventListener('change', onChange))); - } - } - - /** - * Notify the main process about changes to the navigator connection state. - */ - protected override onChangeBrowserConnection(): void { - super.onChangeBrowserConnection(); - this._channel.call(MeteredConnectionCommand.SetIsBrowserConnectionMetered, this.isBrowserConnectionMetered); +export class NativeMeteredConnectionService extends MeteredConnectionChannelClient { + constructor(@IMainProcessService mainProcessService: IMainProcessService) { + super(mainProcessService.getChannel(METERED_CONNECTION_CHANNEL)); } } -registerSingleton(IMeteredConnectionService, new SyncDescriptor(NativeMeteredConnectionService, [getIsBrowserConnectionMetered], false)); +registerSingleton(IMeteredConnectionService, NativeMeteredConnectionService, InstantiationType.Delayed); diff --git a/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts b/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts index 4246eaad88872a..2c4dbae97cb779 100644 --- a/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts +++ b/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts @@ -27,9 +27,6 @@ export class MeteredConnectionChannel implements IServerChannel { switch (command) { case MeteredConnectionCommand.IsConnectionMetered: return this.service.isConnectionMetered; - case MeteredConnectionCommand.SetIsBrowserConnectionMetered: - this.service.setIsBrowserConnectionMetered(arg); - break; default: throw new Error(`Call not found: ${command}`); } diff --git a/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts b/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts index c77a2fed7ef906..0975a22f2111bd 100644 --- a/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts +++ b/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts @@ -3,38 +3,103 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DeferredPromise } from '../../../base/common/async.js'; +import type { MeteredConnectionMonitor, MeteredConnectionState } from '@vscode/metered'; +import { DeferredPromise, raceTimeout } from '../../../base/common/async.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { ILogService } from '../../log/common/log.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AbstractMeteredConnectionService } from '../common/meteredConnection.js'; +type MonitorFactory = () => Promise; +const INITIALIZATION_TIMEOUT = 10_000; + +interface Options { + readonly monitorFactory?: MonitorFactory; + readonly initializationTimeout?: number; +} + +async function createMonitor(): Promise { + const { createMonitor } = await import('@vscode/metered'); + return createMonitor(); +} + /** * Electron-main implementation of the metered connection service. - * This implementation receives metered connection updates via IPC channel from the renderer process. + * This implementation receives metered connection updates from the operating system. */ export class MeteredConnectionMainService extends AbstractMeteredConnectionService { private telemetryService: ITelemetryService | undefined; - private readonly connectionStateInitialized = new DeferredPromise(); - readonly whenConnectionStateInitialized = this.connectionStateInitialized.p; + private readonly monitorFactory: MonitorFactory; + private readonly initialized = new DeferredPromise(); + private readonly initializationTimeout: number; + readonly whenConnectionStateInitialized = this.initialized.p; - constructor(@IConfigurationService configurationService: IConfigurationService) { + constructor( + options: Options | undefined, + @IConfigurationService configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService, + ) { super(configurationService, false); + this.monitorFactory = options?.monitorFactory ?? createMonitor; + this.initializationTimeout = options?.initializationTimeout ?? INITIALIZATION_TIMEOUT; } public setTelemetryService(telemetryService: ITelemetryService): void { + const shouldInitialize = this.telemetryService === undefined; this.telemetryService = telemetryService; + if (shouldInitialize) { + void this.initialize(); + } } - public override setIsBrowserConnectionMetered(value: boolean): void { - super.setIsBrowserConnectionMetered(value); - this.connectionStateInitialized.complete(); + private async initialize(): Promise { + try { + const monitor = await this.monitorFactory(); + if (this._store.isDisposed) { + monitor.dispose(); + return; + } + this._register(monitor); + + let receivedChange = false; + this._register(monitor.onDidChange(state => { + receivedChange = true; + this.updateState(state); + })); + + const initialStateHandled = monitor.ready.then(state => { + if (!receivedChange && !this._store.isDisposed) { + this.updateState(state); + } + return true; + }); + + await raceTimeout(initialStateHandled, this.initializationTimeout, () => { + this.logService.warn(`MeteredConnectionMainService#initialize - Native metered connection monitoring did not initialize within ${this.initializationTimeout}ms`); + }); + } catch (error) { + this.logService.error('MeteredConnectionMainService#initialize - Failed to initialize native metered connection monitoring', error); + } finally { + this.initialized.complete(); + } + } + + private updateState(state: MeteredConnectionState): void { + try { + if (state.status === 'unknown') { + this.logService.info(`MeteredConnectionMainService#updateState - Metered connection state is unknown (source: ${state.source}, reason: ${state.reason ?? 'unspecified'})`); + } + this.setIsUnderlyingConnectionMetered(state.status === 'metered'); + } catch (error) { + this.logService.error('MeteredConnectionMainService#updateState - Failed to apply native metered connection state', error); + } } - protected override onChangeBrowserConnection() { + protected override onChangeUnderlyingConnection() { // Fire event after sending telemetry if switching to metered since telemetry will be paused. - const fireAfter = this.isBrowserConnectionMetered; + const fireAfter = this.isUnderlyingConnectionMetered; if (!fireAfter) { - super.onChangeBrowserConnection(); + super.onChangeUnderlyingConnection(); } type MeteredConnectionStateChangeEvent = { @@ -46,11 +111,11 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi connectionState: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the underlying network connection is metered according to the OS.' }; }; this.telemetryService?.publicLog2('meteredConnectionStateChange', { - connectionState: this.isBrowserConnectionMetered, + connectionState: this.isUnderlyingConnectionMetered, }); if (fireAfter) { - super.onChangeBrowserConnection(); + super.onChangeUnderlyingConnection(); } } } diff --git a/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts b/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts index 36355c464b94c7..8df414ed20bd5a 100644 --- a/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts +++ b/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts @@ -4,48 +4,85 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { DeferredPromise, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { mock } from '../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { IMainProcessService } from '../../../ipc/common/mainProcessService.js'; import { METERED_CONNECTION_CHANNEL, MeteredConnectionCommand } from '../../common/meteredConnectionIpc.js'; import { NativeMeteredConnectionService } from '../../electron-browser/meteredConnectionService.js'; -class TestChannel implements IChannel { +class TestChannel implements IChannel, IDisposable { readonly calls: { command: string; argument: unknown }[] = []; + private readonly onDidChangeEmitter = new Emitter(); + + constructor(private readonly initialState: Promise = Promise.resolve(true)) { } call(command: string, arg?: unknown, _cancellationToken?: CancellationToken): Promise { this.calls.push({ command, argument: arg }); - return Promise.resolve(undefined as T); + return this.initialState as Promise; } - listen(_event: string, _arg?: unknown): Event { + listen(event: string, _arg?: unknown): Event { + if (event === MeteredConnectionCommand.OnDidChangeIsConnectionMetered) { + return this.onDidChangeEmitter.event as Event; + } return Event.None; } + + fire(value: boolean): void { + this.onDidChangeEmitter.fire(value); + } + + dispose(): void { + this.onDidChangeEmitter.dispose(); + } } suite('NativeMeteredConnectionService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); - test('reports the initial browser connection state to the main process', () => { - const channel = new TestChannel(); + test('receives the initial native connection state from the main process', async () => { + const channel = store.add(new TestChannel()); const mainProcessService = new class extends mock() { override getChannel(channelName: string): IChannel { assert.strictEqual(channelName, METERED_CONNECTION_CHANNEL); return channel; } }; - const configurationService = new TestConfigurationService(); - store.add(configurationService.onDidChangeConfigurationEmitter); - store.add(new NativeMeteredConnectionService(() => true, configurationService, mainProcessService)); + const service = store.add(new NativeMeteredConnectionService(mainProcessService)); + await timeout(0); + + assert.deepStrictEqual({ + calls: channel.calls, + isConnectionMetered: service.isConnectionMetered, + }, { + calls: [{ + command: MeteredConnectionCommand.IsConnectionMetered, + argument: undefined, + }], + isConnectionMetered: true, + }); + }); + + test('does not overwrite an event with a stale initial state', async () => { + const initialState = new DeferredPromise(); + const channel = store.add(new TestChannel(initialState.p)); + const mainProcessService = new class extends mock() { + override getChannel(): IChannel { + return channel; + } + }; + const service = store.add(new NativeMeteredConnectionService(mainProcessService)); + + channel.fire(true); + initialState.complete(false); + await timeout(0); - assert.deepStrictEqual(channel.calls, [{ - command: MeteredConnectionCommand.SetIsBrowserConnectionMetered, - argument: true, - }]); + assert.strictEqual(service.isConnectionMetered, true); }); }); diff --git a/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts b/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts index 6834a5d80f814d..a6a7c72ec57efe 100644 --- a/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts +++ b/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts @@ -3,26 +3,59 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { MeteredConnectionMonitor, MeteredConnectionState } from '@vscode/metered'; import assert from 'assert'; -import { timeout } from '../../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../../base/common/async.js'; +import { Emitter } from '../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; +import { NullLogService } from '../../../log/common/log.js'; +import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; import { MeteredConnectionMainService } from '../../electron-main/meteredConnectionMainService.js'; +class TestMeteredConnectionMonitor implements MeteredConnectionMonitor { + private readonly onDidChangeEmitter = new Emitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + private resolveReady!: (state: MeteredConnectionState) => void; + readonly ready = new Promise(resolve => this.resolveReady = resolve); + current: MeteredConnectionState | undefined; + disposeCount = 0; + + setInitialState(state: MeteredConnectionState): void { + this.current = state; + this.resolveReady(state); + } + + setState(state: MeteredConnectionState): void { + this.current = state; + this.onDidChangeEmitter.fire(state); + } + + dispose(): void { + this.disposeCount++; + this.onDidChangeEmitter.dispose(); + } +} + suite('MeteredConnectionMainService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); - test('initialization waits for the initial browser connection state', async () => { + test('initialization waits for the initial native connection state', async () => { const configurationService = new TestConfigurationService(); store.add(configurationService.onDidChangeConfigurationEmitter); - const service = store.add(new MeteredConnectionMainService(configurationService)); + const monitor = new TestMeteredConnectionMonitor(); + const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); let initialized = false; void service.whenConnectionStateInitialized.then(() => initialized = true); await timeout(0); assert.strictEqual(initialized, false); - service.setIsBrowserConnectionMetered(true); + monitor.setInitialState({ + status: 'metered', + source: 'windows-network-cost-manager', + }); await service.whenConnectionStateInitialized; assert.deepStrictEqual({ @@ -33,4 +66,76 @@ suite('MeteredConnectionMainService', () => { isConnectionMetered: true, }); }); + + test('reacts to native connection state changes', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const monitor = new TestMeteredConnectionMonitor(); + const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); + const changes: boolean[] = []; + store.add(service.onDidChangeIsConnectionMetered(state => changes.push(state))); + + monitor.setInitialState({ + status: 'unmetered', + source: 'linux-network-manager', + details: { meteredState: 'no' }, + }); + await service.whenConnectionStateInitialized; + monitor.setState({ + status: 'metered', + source: 'linux-network-manager', + details: { meteredState: 'guessYes' }, + }); + monitor.setState({ + status: 'unknown', + source: 'unsupported', + reason: 'serviceUnavailable', + }); + + assert.deepStrictEqual({ + isConnectionMetered: service.isConnectionMetered, + changes, + }, { + isConnectionMetered: false, + changes: [true, false], + }); + }); + + test('completes initialization on timeout and accepts the late native state', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const monitor = new TestMeteredConnectionMonitor(); + const service = store.add(new MeteredConnectionMainService({ + monitorFactory: async () => monitor, + initializationTimeout: 0, + }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); + + await service.whenConnectionStateInitialized; + monitor.setInitialState({ + status: 'metered', + source: 'macos-network-framework', + available: true, + details: { expensive: false, constrained: true }, + }); + await timeout(0); + + assert.strictEqual(service.isConnectionMetered, true); + }); + + test('disposes a monitor created after the service was disposed', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const monitor = new TestMeteredConnectionMonitor(); + const monitorPromise = new DeferredPromise(); + const service = store.add(new MeteredConnectionMainService({ monitorFactory: () => monitorPromise.p }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); + + service.dispose(); + monitorPromise.complete(monitor); + await service.whenConnectionStateInitialized; + + assert.strictEqual(monitor.disposeCount, 1); + }); }); From aaa2313438cd16216530ac03f1442a579cb3fa61 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Fri, 21 Aug 2026 02:13:26 -0700 Subject: [PATCH 02/11] meteredConnection: await initial state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 2 +- src/vs/code/electron-main/app.ts | 4 +- .../common/meteredConnection.ts | 7 +- .../common/meteredConnectionIpc.ts | 16 ++- .../electron-main/meteredConnectionChannel.ts | 1 + .../meteredConnectionMainService.ts | 66 +++++++----- .../meteredConnectionService.test.ts | 56 +++++++++- .../meteredConnectionMainService.test.ts | 102 ++++++++++++++++-- .../telemetry/common/telemetryService.ts | 54 +++++++--- .../test/browser/telemetryService.test.ts | 78 +++++++++++++- .../electron-main/abstractUpdateService.ts | 2 +- .../abstractUpdateService.test.ts | 2 +- .../common/userDataAutoSyncService.ts | 13 ++- .../test/common/userDataSyncClient.ts | 2 +- .../browser/mainThreadMeteredConnection.ts | 12 ++- .../api/common/extHostMeteredConnection.ts | 4 +- .../common/extHostMeteredConnection.test.ts | 32 ++++++ .../contrib/chat/browser/pluginAutoUpdate.ts | 1 + .../plugins/pluginMarketplaceService.ts | 14 ++- .../browser/plugins/pluginAutoUpdate.test.ts | 29 ++++- .../plugins/pluginMarketplaceService.test.ts | 42 +++++++- .../browser/extensionsWorkbenchService.ts | 37 ++++--- .../extensionRecommendationsService.test.ts | 2 +- .../extensionsActions.test.ts | 2 +- .../electron-browser/extensionsViews.test.ts | 2 +- .../extensionsWorkbenchService.test.ts | 28 ++++- .../browser/meteredConnectionStatus.ts | 11 +- .../update/browser/postUpdateWidget.ts | 3 +- .../electron-browser/postUpdateWidget.test.ts | 18 +++- ...scode.proposed.envIsConnectionMetered.d.ts | 2 + 30 files changed, 544 insertions(+), 100 deletions(-) create mode 100644 src/vs/workbench/api/test/common/extHostMeteredConnection.test.ts diff --git a/package-lock.json b/package-lock.json index c5e9841a232a7e..753affe5c98e70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4629,7 +4629,7 @@ "node_modules/@vscode/metered": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@vscode/metered/-/metered-0.1.0.tgz", - "integrity": "sha512-UOyIf3bdQTPEaXuqPVP617p5V90hrdS7eWQU+GoQ4bcCIv9MwZkBqWIoq7qucGvTX7YpIDwhGKvZjtJFbgbMRg==", + "integrity": "sha512-u8wPCGycvpzADInvEGFZgDBR/d7EQ1U+rxwFIoUGmOMk7BxbeQzYGRLIXhgcczOPQnL8fjJzTddArB0gjsHWNQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f6488c3ce535ed..ee79d1aaceb8e3 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -742,7 +742,9 @@ export class CodeApplication extends Disposable { // Metered connection telemetry appInstantiationService.invokeFunction(accessor => { - (accessor.get(IMeteredConnectionService) as MeteredConnectionMainService).setTelemetryService(accessor.get(ITelemetryService)); + const meteredConnectionService = accessor.get(IMeteredConnectionService) as MeteredConnectionMainService; + meteredConnectionService.setTelemetryService(accessor.get(ITelemetryService)); + meteredConnectionService.start(); }); // Auth Handler diff --git a/src/vs/platform/meteredConnection/common/meteredConnection.ts b/src/vs/platform/meteredConnection/common/meteredConnection.ts index b97c48738c72a0..f892b929a5c585 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnection.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnection.ts @@ -20,13 +20,14 @@ export interface IMeteredConnectionService { * Whether the current network connection is metered. * Always returns `false` if the `network.meteredConnection` setting is `off`. * Always returns `true` if the `network.meteredConnection` setting is `on`. + * Implementations may conservatively return `true` until {@link whenInitialized} resolves. */ readonly isConnectionMetered: boolean; /** - * Resolves once the initial connection state is available, when initialization is asynchronous. + * Resolves once the initial connection state is available. */ - readonly whenConnectionStateInitialized?: Promise; + readonly whenInitialized: Promise; /** * Event that fires when the metered connection status changes. @@ -43,6 +44,8 @@ export type MeteredConnectionSettingValue = 'on' | 'off' | 'auto'; export abstract class AbstractMeteredConnectionService extends Disposable implements IMeteredConnectionService { declare readonly _serviceBrand: undefined; + public readonly whenInitialized = Promise.resolve(); + private readonly _onDidChangeIsConnectionMetered = this._register(new Emitter()); public readonly onDidChangeIsConnectionMetered = this._onDidChangeIsConnectionMetered.event; diff --git a/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts b/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts index 5c4ea0d164acdc..8ac0aae0a6d10d 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnectionIpc.ts @@ -28,11 +28,13 @@ export class MeteredConnectionChannelClient extends Disposable implements IMeter private readonly _onDidChangeIsConnectionMetered = this._register(new Emitter()); public readonly onDidChangeIsConnectionMetered = this._onDidChangeIsConnectionMetered.event; - private _isConnectionMetered = false; + private _isConnectionMetered = true; public get isConnectionMetered(): boolean { return this._isConnectionMetered; } + public readonly whenInitialized: Promise; + constructor(channel: IChannel) { super(); @@ -45,11 +47,15 @@ export class MeteredConnectionChannelClient extends Disposable implements IMeter } })); - channel.call(MeteredConnectionCommand.IsConnectionMetered).then(value => { - if (!receivedEvent && this._isConnectionMetered !== value) { + this.whenInitialized = channel.call(MeteredConnectionCommand.IsConnectionMetered).then(value => { + if (!receivedEvent) { this._isConnectionMetered = value; - this._onDidChangeIsConnectionMetered.fire(value); } - }, onUnexpectedError); + }, error => { + onUnexpectedError(error); + if (!receivedEvent) { + this._isConnectionMetered = false; + } + }); } } diff --git a/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts b/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts index 2c4dbae97cb779..1c1377fa21dcad 100644 --- a/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts +++ b/src/vs/platform/meteredConnection/electron-main/meteredConnectionChannel.ts @@ -26,6 +26,7 @@ export class MeteredConnectionChannel implements IServerChannel { public async call(_: unknown, command: string, arg?: any): Promise { switch (command) { case MeteredConnectionCommand.IsConnectionMetered: + await this.service.whenInitialized; return this.service.isConnectionMetered; default: throw new Error(`Call not found: ${command}`); diff --git a/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts b/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts index 0975a22f2111bd..1b6f724baedb65 100644 --- a/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts +++ b/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts @@ -32,7 +32,8 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi private readonly monitorFactory: MonitorFactory; private readonly initialized = new DeferredPromise(); private readonly initializationTimeout: number; - readonly whenConnectionStateInitialized = this.initialized.p; + private started = false; + override readonly whenInitialized = this.initialized.p; constructor( options: Options | undefined, @@ -45,53 +46,64 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi } public setTelemetryService(telemetryService: ITelemetryService): void { - const shouldInitialize = this.telemetryService === undefined; this.telemetryService = telemetryService; - if (shouldInitialize) { + } + + public start(): void { + if (!this.started) { + this.started = true; void this.initialize(); } } + override dispose(): void { + this.initialized.complete(); + super.dispose(); + } + private async initialize(): Promise { + const initialization = this.doInitialize().catch(error => { + this.logService.error('MeteredConnectionMainService#initialize - Failed to initialize native metered connection monitoring', error); + }); try { - const monitor = await this.monitorFactory(); - if (this._store.isDisposed) { - monitor.dispose(); - return; - } - this._register(monitor); - - let receivedChange = false; - this._register(monitor.onDidChange(state => { - receivedChange = true; - this.updateState(state); - })); - - const initialStateHandled = monitor.ready.then(state => { - if (!receivedChange && !this._store.isDisposed) { - this.updateState(state); - } - return true; - }); - - await raceTimeout(initialStateHandled, this.initializationTimeout, () => { + await raceTimeout(initialization, this.initializationTimeout, () => { this.logService.warn(`MeteredConnectionMainService#initialize - Native metered connection monitoring did not initialize within ${this.initializationTimeout}ms`); }); - } catch (error) { - this.logService.error('MeteredConnectionMainService#initialize - Failed to initialize native metered connection monitoring', error); } finally { this.initialized.complete(); } } - private updateState(state: MeteredConnectionState): void { + private async doInitialize(): Promise { + const monitor = await this.monitorFactory(); + if (this._store.isDisposed) { + monitor.dispose(); + return; + } + this._register(monitor); + + let receivedDefinitiveChange = false; + this._register(monitor.onDidChange(state => { + receivedDefinitiveChange = this.updateState(state) || receivedDefinitiveChange; + })); + + const state = await monitor.ready; + if (!receivedDefinitiveChange && !this._store.isDisposed) { + this.updateState(state); + } + } + + private updateState(state: MeteredConnectionState): boolean { try { if (state.status === 'unknown') { this.logService.info(`MeteredConnectionMainService#updateState - Metered connection state is unknown (source: ${state.source}, reason: ${state.reason ?? 'unspecified'})`); + return false; } this.setIsUnderlyingConnectionMetered(state.status === 'metered'); + return true; } catch (error) { this.logService.error('MeteredConnectionMainService#updateState - Failed to apply native metered connection state', error); + return false; } } diff --git a/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts b/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts index 8df414ed20bd5a..6c06099f4891e6 100644 --- a/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts +++ b/src/vs/platform/meteredConnection/test/electron-browser/meteredConnectionService.test.ts @@ -6,6 +6,7 @@ import assert from 'assert'; import { DeferredPromise, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; @@ -55,7 +56,7 @@ suite('NativeMeteredConnectionService', () => { }; const service = store.add(new NativeMeteredConnectionService(mainProcessService)); - await timeout(0); + await service.whenInitialized; assert.deepStrictEqual({ calls: channel.calls, @@ -78,11 +79,62 @@ suite('NativeMeteredConnectionService', () => { } }; const service = store.add(new NativeMeteredConnectionService(mainProcessService)); + let initialized = false; + void service.whenInitialized.then(() => initialized = true); + + await timeout(0); + assert.strictEqual(initialized, false); channel.fire(true); initialState.complete(false); - await timeout(0); + await service.whenInitialized; + + assert.deepStrictEqual({ + initialized, + isConnectionMetered: service.isConnectionMetered, + }, { + initialized: true, + isConnectionMetered: true, + }); + }); + + test('uses a conservative pending state without firing an initial change event', async () => { + const initialState = new DeferredPromise(); + const channel = store.add(new TestChannel(initialState.p)); + const mainProcessService = new class extends mock() { + override getChannel(): IChannel { + return channel; + } + }; + const service = store.add(new NativeMeteredConnectionService(mainProcessService)); + const changes: boolean[] = []; + store.add(service.onDidChangeIsConnectionMetered(value => changes.push(value))); + + assert.strictEqual(service.isConnectionMetered, true); + + initialState.complete(false); + await service.whenInitialized; + + assert.deepStrictEqual({ + isConnectionMetered: service.isConnectionMetered, + changes, + }, { + isConnectionMetered: false, + changes: [], + }); + }); + + test('falls back to unmetered when the initial state request fails', async () => { + const channel = store.add(new TestChannel(Promise.reject(new CancellationError()))); + const mainProcessService = new class extends mock() { + override getChannel(): IChannel { + return channel; + } + }; + const service = store.add(new NativeMeteredConnectionService(mainProcessService)); assert.strictEqual(service.isConnectionMetered, true); + await service.whenInitialized; + assert.strictEqual(service.isConnectionMetered, false); }); }); diff --git a/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts b/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts index a6a7c72ec57efe..9a168b420ff9c9 100644 --- a/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts +++ b/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts @@ -11,6 +11,8 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { NullLogService } from '../../../log/common/log.js'; import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; +import { MeteredConnectionCommand } from '../../common/meteredConnectionIpc.js'; +import { MeteredConnectionChannel } from '../../electron-main/meteredConnectionChannel.js'; import { MeteredConnectionMainService } from '../../electron-main/meteredConnectionMainService.js'; class TestMeteredConnectionMonitor implements MeteredConnectionMonitor { @@ -46,8 +48,9 @@ suite('MeteredConnectionMainService', () => { const monitor = new TestMeteredConnectionMonitor(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); service.setTelemetryService(NullTelemetryService); + service.start(); let initialized = false; - void service.whenConnectionStateInitialized.then(() => initialized = true); + void service.whenInitialized.then(() => initialized = true); await timeout(0); assert.strictEqual(initialized, false); @@ -56,7 +59,7 @@ suite('MeteredConnectionMainService', () => { status: 'metered', source: 'windows-network-cost-manager', }); - await service.whenConnectionStateInitialized; + await service.whenInitialized; assert.deepStrictEqual({ initialized, @@ -73,6 +76,7 @@ suite('MeteredConnectionMainService', () => { const monitor = new TestMeteredConnectionMonitor(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); service.setTelemetryService(NullTelemetryService); + service.start(); const changes: boolean[] = []; store.add(service.onDidChangeIsConnectionMetered(state => changes.push(state))); @@ -81,7 +85,7 @@ suite('MeteredConnectionMainService', () => { source: 'linux-network-manager', details: { meteredState: 'no' }, }); - await service.whenConnectionStateInitialized; + await service.whenInitialized; monitor.setState({ status: 'metered', source: 'linux-network-manager', @@ -97,9 +101,34 @@ suite('MeteredConnectionMainService', () => { isConnectionMetered: service.isConnectionMetered, changes, }, { - isConnectionMetered: false, - changes: [true, false], + isConnectionMetered: true, + changes: [true], + }); + }); + + test('channel initial state waits for native initialization', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const monitor = new TestMeteredConnectionMonitor(); + const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); + service.start(); + const channel = new MeteredConnectionChannel(service); + let resolved = false; + const initialState = channel.call(undefined, MeteredConnectionCommand.IsConnectionMetered).then(value => { + resolved = true; + return value; + }); + + await timeout(0); + assert.strictEqual(resolved, false); + + monitor.setInitialState({ + status: 'metered', + source: 'windows-network-cost-manager', }); + + assert.strictEqual(await initialState, true); }); test('completes initialization on timeout and accepts the late native state', async () => { @@ -111,8 +140,9 @@ suite('MeteredConnectionMainService', () => { initializationTimeout: 0, }, configurationService, new NullLogService())); service.setTelemetryService(NullTelemetryService); + service.start(); - await service.whenConnectionStateInitialized; + await service.whenInitialized; monitor.setInitialState({ status: 'metered', source: 'macos-network-framework', @@ -124,6 +154,52 @@ suite('MeteredConnectionMainService', () => { assert.strictEqual(service.isConnectionMetered, true); }); + test('initialization timeout includes monitor creation', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const monitor = new TestMeteredConnectionMonitor(); + const monitorPromise = new DeferredPromise(); + const service = store.add(new MeteredConnectionMainService({ + monitorFactory: () => monitorPromise.p, + initializationTimeout: 0, + }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); + service.start(); + + await service.whenInitialized; + monitorPromise.complete(monitor); + monitor.setInitialState({ + status: 'metered', + source: 'windows-network-cost-manager', + }); + await timeout(0); + + assert.strictEqual(service.isConnectionMetered, true); + }); + + test('definitive ready state is applied after an unknown change', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const monitor = new TestMeteredConnectionMonitor(); + const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); + service.setTelemetryService(NullTelemetryService); + service.start(); + await timeout(0); + + monitor.setState({ + status: 'unknown', + source: 'unsupported', + reason: 'serviceUnavailable', + }); + monitor.setInitialState({ + status: 'metered', + source: 'windows-network-cost-manager', + }); + await service.whenInitialized; + + assert.strictEqual(service.isConnectionMetered, true); + }); + test('disposes a monitor created after the service was disposed', async () => { const configurationService = new TestConfigurationService(); store.add(configurationService.onDidChangeConfigurationEmitter); @@ -131,11 +207,23 @@ suite('MeteredConnectionMainService', () => { const monitorPromise = new DeferredPromise(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: () => monitorPromise.p }, configurationService, new NullLogService())); service.setTelemetryService(NullTelemetryService); + service.start(); service.dispose(); monitorPromise.complete(monitor); - await service.whenConnectionStateInitialized; + await service.whenInitialized; + await timeout(0); assert.strictEqual(monitor.disposeCount, 1); }); + + test('dispose completes initialization before start', async () => { + const configurationService = new TestConfigurationService(); + store.add(configurationService.onDidChangeConfigurationEmitter); + const service = new MeteredConnectionMainService(undefined, configurationService, new NullLogService()); + + service.dispose(); + + await service.whenInitialized; + }); }); diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index c72b9f741803ed..9abe9dcb0dc3f6 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -66,10 +66,12 @@ export class TelemetryService implements ITelemetryService { private _sendErrorTelemetry: boolean; private readonly _meteredConnectionService: IMeteredConnectionService | undefined; + private _isMeteredConnectionInitialized: boolean; private _pendingEvents: IPendingEvent[] = []; private _isExperimentPropertySet = false; private _flushTimeout: ReturnType | undefined; + private _isDisposed = false; private readonly _disposables = new DisposableStore(); private _cleanupPatterns: RegExp[] = []; @@ -93,6 +95,15 @@ export class TelemetryService implements ITelemetryService { this._telemetryLevel = TelemetryLevel.USAGE; this._sendErrorTelemetry = !!config.sendErrorTelemetry; this._meteredConnectionService = config.meteredConnectionService; + this._isMeteredConnectionInitialized = !this._meteredConnectionService; + if (this._meteredConnectionService) { + void this._meteredConnectionService.whenInitialized.then(() => { + if (!this._isDisposed) { + this._isMeteredConnectionInitialized = true; + this._flushPendingEventsIfReady(); + } + }); + } // static cleanup pattern for: `vscode-file:///DANGEROUS/PATH/resources/app/Useful/Information` this._cleanupPatterns = [/(vscode-)?file:\/\/.*?\/resources\/app\//gi]; @@ -139,19 +150,29 @@ export class TelemetryService implements ITelemetryService { this._commonProperties[name] = value; } - private _flushPendingEvents(): void { - if (this._isExperimentPropertySet) { - return; + private _flushPendingEvents(force = false): void { + if (!this._isExperimentPropertySet) { + this._isExperimentPropertySet = true; + + if (this._flushTimeout !== undefined) { + clearTimeout(this._flushTimeout); + this._flushTimeout = undefined; + } } - this._isExperimentPropertySet = true; + this._flushPendingEventsIfReady(force); + } + + private _flushPendingEventsIfReady(force = false): void { + if (!this._isExperimentPropertySet || (!this._isMeteredConnectionInitialized && !force)) { + return; + } - if (this._flushTimeout !== undefined) { - clearTimeout(this._flushTimeout); - this._flushTimeout = undefined; + if (this._meteredConnectionService?.isConnectionMetered) { + this._pendingEvents = []; + return; } - // Send all buffered events now that experiment properties are available for (const event of this._pendingEvents) { this._doLog(event.eventName, event.eventLevel, event.data); } @@ -181,8 +202,9 @@ export class TelemetryService implements ITelemetryService { } dispose(): void { + this._isDisposed = true; // Flush any remaining pending events before disposing - this._flushPendingEvents(); + this._flushPendingEvents(true); this._disposables.dispose(); } @@ -192,19 +214,19 @@ export class TelemetryService implements ITelemetryService { return; } - // Don't send events when the connection is metered - if (this._meteredConnectionService?.isConnectionMetered) { - return; - } - - // Buffer events until experiment properties are set (or timeout expires) - if (!this._isExperimentPropertySet) { + // Buffer events until experiment properties and the initial metered connection state are available. + if (!this._isExperimentPropertySet || !this._isMeteredConnectionInitialized) { if (this._pendingEvents.length < TelemetryService.MAX_BUFFER_SIZE) { this._pendingEvents.push({ eventName, eventLevel, data }); } return; } + // Don't send events when the connection is metered + if (this._meteredConnectionService?.isConnectionMetered) { + return; + } + this._doLog(eventName, eventLevel, data); } diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index baf364cd13dae4..8b45f8bdd983d7 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -6,10 +6,12 @@ import assert from 'assert'; import * as sinon from 'sinon'; import sinonTest from 'sinon-test'; import { mainWindow } from '../../../../base/browser/window.js'; +import { DeferredPromise } from '../../../../base/common/async.js'; import * as Errors from '../../../../base/common/errors.js'; -import { Emitter } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; +import { IMeteredConnectionService } from '../../../meteredConnection/common/meteredConnection.js'; import product from '../../../product/common/product.js'; import { IProductService } from '../../../product/common/productService.js'; import ErrorTelemetry from '../../browser/errorTelemetry.js'; @@ -43,6 +45,16 @@ class TestTelemetryAppender implements ITelemetryAppender { } } +class TestMeteredConnectionService implements IMeteredConnectionService { + declare readonly _serviceBrand: undefined; + readonly onDidChangeIsConnectionMetered = Event.None; + + constructor( + readonly isConnectionMetered: boolean, + readonly whenInitialized: Promise, + ) { } +} + class ErrorTestingSettings { public personalInfo: string; public importantInfo: string; @@ -137,6 +149,70 @@ suite('TelemetryService', () => { service.dispose(); })); + test('buffers events until the metered connection state is initialized', async () => { + const initialized = new DeferredPromise(); + const testAppender = new TestTelemetryAppender(); + const service = new TelemetryService({ + appenders: [testAppender], + meteredConnectionService: new TestMeteredConnectionService(false, initialized.p), + }, new TestConfigurationService(), TestProductService); + + service.publicLog('testEvent'); + assert.strictEqual(testAppender.getEventsCount(), 0); + + initialized.complete(); + await initialized.p; + await Promise.resolve(); + + assert.strictEqual(testAppender.getEventsCount(), 1); + service.dispose(); + }); + + test('drops buffered events when the initialized connection is metered', async () => { + const initialized = new DeferredPromise(); + const testAppender = new TestTelemetryAppender(); + const service = new TelemetryService({ + appenders: [testAppender], + meteredConnectionService: new TestMeteredConnectionService(true, initialized.p), + }, new TestConfigurationService(), TestProductService); + + service.publicLog('testEvent'); + initialized.complete(); + await initialized.p; + await Promise.resolve(); + + assert.strictEqual(testAppender.getEventsCount(), 0); + service.dispose(); + }); + + test('flushes buffered events on dispose while the metered connection state is pending', () => { + const initialized = new DeferredPromise(); + const testAppender = new TestTelemetryAppender(); + const service = new TelemetryService({ + appenders: [testAppender], + meteredConnectionService: new TestMeteredConnectionService(false, initialized.p), + }, new TestConfigurationService(), TestProductService); + + service.publicLog('testEvent'); + service.dispose(); + + assert.strictEqual(testAppender.getEventsCount(), 1); + }); + + test('drops buffered events on dispose while the pending connection state is conservatively metered', () => { + const initialized = new DeferredPromise(); + const testAppender = new TestTelemetryAppender(); + const service = new TelemetryService({ + appenders: [testAppender], + meteredConnectionService: new TestMeteredConnectionService(true, initialized.p), + }, new TestConfigurationService(), TestProductService); + + service.publicLog('testEvent'); + service.dispose(); + + assert.strictEqual(testAppender.getEventsCount(), 0); + }); + test('Event with data', sinonTestFn(function () { const testAppender = new TestTelemetryAppender(); const service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 2749d7beb453cb..d21a18373a4c8e 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -209,7 +209,7 @@ export abstract class AbstractUpdateService extends Disposable implements IUpdat return; } - await this.meteredConnectionService.whenConnectionStateInitialized; + await this.meteredConnectionService.whenInitialized; // React to runtime `update.mode`/policy changes so switching to/from `none` applies without a restart. this._register(this.configurationService.onDidChangeConfiguration(e => { diff --git a/src/vs/platform/update/test/electron-main/abstractUpdateService.test.ts b/src/vs/platform/update/test/electron-main/abstractUpdateService.test.ts index a493bdb603d777..3f1f87947f33af 100644 --- a/src/vs/platform/update/test/electron-main/abstractUpdateService.test.ts +++ b/src/vs/platform/update/test/electron-main/abstractUpdateService.test.ts @@ -31,7 +31,7 @@ class TestMeteredConnectionService extends Disposable implements IMeteredConnect constructor( public isConnectionMetered: boolean, - readonly whenConnectionStateInitialized?: Promise, + readonly whenInitialized: Promise = Promise.resolve(), ) { super(); } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 4d60c3b4305fdd..8fb9b2e40f48c0 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -105,7 +105,11 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } else { this.logService.info('[AutoSync] Disabled.'); } - this.updateAutoSync(); + if (this.meteredConnectionService.isConnectionMetered) { + void this.initializeAutoSync(); + } else { + this.updateAutoSync(); + } if (this.hasToDisableMachineEventually()) { this.disableMachineEventually(); @@ -120,6 +124,13 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } } + private async initializeAutoSync(): Promise { + await this.meteredConnectionService.whenInitialized; + if (!this._store.isDisposed) { + this.updateAutoSync(); + } + } + private updateAutoSync(): void { const { enabled, message } = this.isAutoSyncEnabled(); if (enabled) { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 058eab04cfb526..ba543566b148bd 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -102,7 +102,7 @@ export class UserDataSyncClient extends Disposable { await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); - this.instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: new Emitter().event }); + this.instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: new Emitter().event }); this.instantiationService.stub(IRequestService, this.testServer); diff --git a/src/vs/workbench/api/browser/mainThreadMeteredConnection.ts b/src/vs/workbench/api/browser/mainThreadMeteredConnection.ts index 5c2fe0ba20ac58..a63b44417c9a0a 100644 --- a/src/vs/workbench/api/browser/mainThreadMeteredConnection.ts +++ b/src/vs/workbench/api/browser/mainThreadMeteredConnection.ts @@ -21,12 +21,18 @@ export class MainThreadMeteredConnection extends Disposable implements MainThrea this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostMeteredConnection); - // Send initial value - this._proxy.$initializeIsConnectionMetered(this.meteredConnectionService.isConnectionMetered); - // Listen for changes and forward to extension host this._register(this.meteredConnectionService.onDidChangeIsConnectionMetered(isMetered => { this._proxy.$onDidChangeIsConnectionMetered(isMetered); })); + + void this.initialize(); + } + + private async initialize(): Promise { + await this.meteredConnectionService.whenInitialized; + if (!this._store.isDisposed) { + this._proxy.$initializeIsConnectionMetered(this.meteredConnectionService.isConnectionMetered); + } } } diff --git a/src/vs/workbench/api/common/extHostMeteredConnection.ts b/src/vs/workbench/api/common/extHostMeteredConnection.ts index 821a58db7b874c..63e238facd8b41 100644 --- a/src/vs/workbench/api/common/extHostMeteredConnection.ts +++ b/src/vs/workbench/api/common/extHostMeteredConnection.ts @@ -20,7 +20,7 @@ export class ExtHostMeteredConnection extends Disposable implements IExtHostMete declare readonly _serviceBrand: undefined; - private _isConnectionMetered: boolean = false; + private _isConnectionMetered: boolean = true; private readonly _onDidChangeIsConnectionMetered = this._register(new Emitter()); readonly onDidChangeIsConnectionMetered: Event = this._onDidChangeIsConnectionMetered.event; @@ -34,7 +34,7 @@ export class ExtHostMeteredConnection extends Disposable implements IExtHostMete } $initializeIsConnectionMetered(isMetered: boolean): void { - this._isConnectionMetered = isMetered; + this.$onDidChangeIsConnectionMetered(isMetered); } $onDidChangeIsConnectionMetered(isMetered: boolean): void { diff --git a/src/vs/workbench/api/test/common/extHostMeteredConnection.test.ts b/src/vs/workbench/api/test/common/extHostMeteredConnection.test.ts new file mode 100644 index 00000000000000..7bc30e24f9b3f3 --- /dev/null +++ b/src/vs/workbench/api/test/common/extHostMeteredConnection.test.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { ExtHostMeteredConnection } from '../../common/extHostMeteredConnection.js'; + +suite('ExtHostMeteredConnection', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('initialization corrects the conservative state and fires a change', () => { + const service = store.add(new ExtHostMeteredConnection()); + const changes: boolean[] = []; + store.add(service.onDidChangeIsConnectionMetered(value => changes.push(value))); + + assert.strictEqual(service.isConnectionMetered, true); + + service.$initializeIsConnectionMetered(false); + service.$initializeIsConnectionMetered(false); + service.$onDidChangeIsConnectionMetered(true); + + assert.deepStrictEqual({ + isConnectionMetered: service.isConnectionMetered, + changes, + }, { + isConnectionMetered: true, + changes: [false, true], + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts index 712cb3c1d84954..a1c6e97a4244c4 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts @@ -64,6 +64,7 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi } private async _triggerAutoUpdate(marketplaceIds: ReadonlySet): Promise { + await this._meteredConnectionService.whenInitialized; if (this._store.isDisposed || this._updateInFlight || this._meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index 141051b8be9346..00550147d35bdf 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -410,7 +410,7 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke this._register(runWhenGlobalIdle(() => { this._updateChecksInitialized = true; - this._scheduleUpdateCheck(); + void this._initializeUpdateChecks(); this._register(Event.filter( _configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(AutoUpdateConfigurationKey) @@ -442,6 +442,13 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke })); } + private async _initializeUpdateChecks(): Promise { + await this._meteredConnectionService.whenInitialized; + if (!this._store.isDisposed) { + this._scheduleUpdateCheck(); + } + } + clearUpdatesAvailable(marketplaceIds?: ReadonlySet): void { const remaining = marketplaceIds ? new Set([...this._marketplacesWithUpdates.get()].filter(id => !marketplaceIds.has(id))) @@ -872,6 +879,11 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke } private async _doRunUpdateCheck(): Promise { + await this._meteredConnectionService.whenInitialized; + if (this._store.isDisposed) { + return; + } + if (this._meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts index a43f4e286ffce5..bda92afd3df700 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { DeferredPromise } from '../../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; @@ -22,7 +23,10 @@ class TestMeteredConnectionService extends Disposable implements IMeteredConnect private readonly _onDidChangeIsConnectionMetered = this._register(new Emitter()); readonly onDidChangeIsConnectionMetered = this._onDidChangeIsConnectionMetered.event; - constructor(public isConnectionMetered: boolean) { + constructor( + public isConnectionMetered: boolean, + readonly whenInitialized: Promise = Promise.resolve(), + ) { super(); } @@ -42,9 +46,13 @@ suite('PluginAutoUpdate', () => { clearUpdatesAvailableCalls: ReadonlySet[]; } - function createContribution(stateOverrides?: Partial, isConnectionMetered = false): { contribution: PluginAutoUpdate; state: MockState; meteredConnectionService: TestMeteredConnectionService } { + function createContribution( + stateOverrides?: Partial, + isConnectionMetered = false, + whenInitialized: Promise = Promise.resolve(), + ): { contribution: PluginAutoUpdate; state: MockState; meteredConnectionService: TestMeteredConnectionService } { const instantiationService = store.add(new TestInstantiationService()); - const meteredConnectionService = store.add(new TestMeteredConnectionService(isConnectionMetered)); + const meteredConnectionService = store.add(new TestMeteredConnectionService(isConnectionMetered, whenInitialized)); const state: MockState = { marketplacesWithUpdates: observableValue>('test.marketplacesWithUpdates', new Set()), @@ -101,6 +109,21 @@ suite('PluginAutoUpdate', () => { })), [{ silent: true, automatic: true, marketplaceIds: ['github:microsoft/plugins'] }]); }); + test('waits for connection state initialization before updating', async () => { + const initialized = new DeferredPromise(); + const { state } = createContribution(undefined, false, initialized.p); + + state.marketplacesWithUpdates.set(new Set(['github:microsoft/plugins']), undefined); + await flushMicrotasks(); + assert.strictEqual(state.updateAllCalls.length, 0); + + initialized.complete(); + await flushMicrotasks(); + await flushMicrotasks(); + + assert.deepStrictEqual(state.updateAllCalls.map(call => [...call.marketplaceIds ?? []]), [['github:microsoft/plugins']]); + }); + test('retains queued updates while metered and runs them when unmetered', async () => { const { state, meteredConnectionService } = createContribution(undefined, true); diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts index d2b5cd4e7a8c83..e33dc896c4ac19 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts @@ -38,7 +38,10 @@ class TestMeteredConnectionService extends Disposable implements IMeteredConnect private readonly _onDidChangeIsConnectionMetered = this._register(new Emitter()); readonly onDidChangeIsConnectionMetered = this._onDidChangeIsConnectionMetered.event; - constructor(public isConnectionMetered: boolean) { + constructor( + public isConnectionMetered: boolean, + readonly whenInitialized: Promise = Promise.resolve(), + ) { super(); } @@ -51,6 +54,7 @@ class TestMeteredConnectionService extends Disposable implements IMeteredConnect const unmeteredConnectionService: IMeteredConnectionService = { _serviceBrand: undefined, isConnectionMetered: false, + whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None, }; @@ -784,6 +788,41 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => { assert.strictEqual(fetchCount, 1); }); + test('periodic update checking waits for metered connection initialization', async () => { + let runIdle: ((idle: IdleDeadline) => void) | undefined; + store.add(installFakeRunWhenIdle((_target, runner) => { + runIdle = runner; + return Disposable.None; + })); + const initialized = new DeferredPromise(); + const meteredConnectionService = store.add(new TestMeteredConnectionService(false, initialized.p)); + let fetchCount = 0; + const service = createService({ + meteredConnectionService, + pluginRepositoryService: { + fetchRepository: async () => { + fetchCount++; + return false; + }, + }, + }); + service.addInstalledPlugin( + URI.file('/agent-plugins/github.com/microsoft/plugins/my-plugin'), + makePlugin('my-plugin', 'my-plugin'), + ); + + assert.ok(runIdle); + runIdle({ didTimeout: false, timeRemaining: () => 50 }); + await timeout(0); + assert.strictEqual(fetchCount, 0); + + initialized.complete(); + await timeout(0); + await timeout(0); + + assert.strictEqual(fetchCount, 1); + }); + test('defers an overdue check until queued updates are acknowledged', async () => { const updateCheckInterval = 24 * 60 * 60 * 1000; const clock = sinon.useFakeTimers({ now: updateCheckInterval + 1 }); @@ -989,6 +1028,7 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => { assert.ok(runIdle); runIdle({ didTimeout: false, timeRemaining: () => 50 }); await timeout(0); + await timeout(0); assert.deepStrictEqual(fetched, [deferredRef.canonicalId]); await configurationService.setUserConfiguration(ChatConfiguration.StrictMarketplaces, [ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index b60ffca7db1ed4..a242e36179c4d6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1155,14 +1155,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this._onChange.fire(undefined); } if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) { - if (this.isAutoCheckUpdatesEnabled()) { - this.checkForUpdates(`Enabled auto check updates`); - } + void this.checkForUpdatesAutomatically(`Enabled auto check updates`); } })); this._register(this.extensionEnablementService.onEnablementChanged(platformExtensions => { - if (this.isAutoCheckUpdatesEnabled() && this.getAutoUpdateValue() === 'on' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) { - this.checkForUpdates('Extension enablement changed'); + if (this.getAutoUpdateValue() === 'on' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) { + void this.checkForUpdatesAutomatically('Extension enablement changed'); } })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); @@ -1172,22 +1170,16 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension owner: 'sandy081'; comment: 'Report when update check is triggered on product update'; }>('extensions:updatecheckonproductupdate'); - if (this.isAutoCheckUpdatesEnabled()) { - this.checkForUpdates('Product update'); - } + void this.checkForUpdatesAutomatically('Product update'); } })); this._register(this.allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => { - if (this.isAutoCheckUpdatesEnabled()) { - this.checkForUpdates('Allowed extensions changed'); - } + void this.checkForUpdatesAutomatically('Allowed extensions changed'); })); this._register(this.meteredConnectionService.onDidChangeIsConnectionMetered(() => { - if (this.isAutoCheckUpdatesEnabled()) { - this.checkForUpdates('Connection is no longer metered'); - } + void this.checkForUpdatesAutomatically('Connection is no longer metered'); if (isWeb && !this.isAutoUpdateEnabled()) { this.autoUpdateBuiltinExtensions(); } @@ -2225,13 +2217,26 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.configurationService.getValue(AutoCheckUpdatesConfigurationKey); } + private async checkForUpdatesAutomatically(reason?: string): Promise { + await this.meteredConnectionService.whenInitialized; + if (!this._store.isDisposed && this.isAutoCheckUpdatesEnabled()) { + await this.checkForUpdates(reason); + } + } + private eventuallyCheckForUpdates(immediate = false): void { this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { + await this.meteredConnectionService.whenInitialized; + if (this._store.isDisposed) { + return; + } if (this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } - this.eventuallyCheckForUpdates(); + if (!this._store.isDisposed) { + this.eventuallyCheckForUpdates(); + } }, immediate ? 0 : this.getUpdatesCheckInterval()).then(undefined, err => null); } @@ -2248,6 +2253,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateBuiltinExtensions(): Promise { + await this.meteredConnectionService.whenInitialized; if (this.meteredConnectionService.isConnectionMetered) { return; } @@ -2272,6 +2278,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateExtensions(): Promise { + await this.meteredConnectionService.whenInitialized; if (this.meteredConnectionService.isConnectionMetered) { this.logService.trace('[Extensions]: Skipping auto-update because connection is metered'); return; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index d47074727c43c7..81fe63ca88d096 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -296,7 +296,7 @@ suite('ExtensionRecommendationsService Test', () => { }); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index f27872132feffd..20e6edb6cc6ff3 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -160,7 +160,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 2d7bfc8d6780a5..ec56ddbf10d370 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -224,7 +224,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); queryPage = aPage([]); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 65a7e0f251a156..d6b745ea114782 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -5,6 +5,7 @@ import * as sinon from 'sinon'; import assert from 'assert'; +import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey, AutoUpdateDelayConfigurationKey, ExtensionRuntimeActionType, AutoUpdateConfigurationValue } from '../../common/extensions.js'; import { ExtensionsWorkbenchService } from '../../browser/extensionsWorkbenchService.js'; @@ -157,7 +158,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); }); test('test gallery extension', async () => { @@ -1778,6 +1779,31 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); + test('waits for metered connection initialization before checking for updates automatically', async () => { + const initialized = new DeferredPromise(); + instantiationService.stub(IMeteredConnectionService, { + isConnectionMetered: false, + whenInitialized: initialized.p, + onDidChangeIsConnectionMetered: Event.None, + }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a')]); + let getExtensionsCount = 0; + instantiationService.stub(IExtensionGalleryService, 'getExtensions', async () => { + getExtensionsCount++; + return []; + }); + + testObject = await aWorkbenchService(); + await timeout(0); + assert.strictEqual(getExtensionsCount, 0); + + initialized.complete(); + await timeout(0); + await timeout(0); + + assert.strictEqual(getExtensionsCount, 1); + }); + async function aWorkbenchService(): Promise { const workbenchService: ExtensionsWorkbenchService = disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService)); await workbenchService.queryLocal(); diff --git a/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts b/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts index eea62f66bfd193..4b19c67221b8ff 100644 --- a/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts +++ b/src/vs/workbench/contrib/meteredConnection/browser/meteredConnectionStatus.ts @@ -21,11 +21,18 @@ export class MeteredConnectionStatusContribution extends Disposable implements I ) { super(); - this.updateStatusBarEntry(this.meteredConnectionService.isConnectionMetered); - this._register(this.meteredConnectionService.onDidChangeIsConnectionMetered(isMetered => { this.updateStatusBarEntry(isMetered); })); + + void this.initialize(); + } + + private async initialize(): Promise { + await this.meteredConnectionService.whenInitialized; + if (!this._store.isDisposed) { + this.updateStatusBarEntry(this.meteredConnectionService.isConnectionMetered); + } } private updateStatusBarEntry(isMetered: boolean): void { diff --git a/src/vs/workbench/contrib/update/browser/postUpdateWidget.ts b/src/vs/workbench/contrib/update/browser/postUpdateWidget.ts index dae0b9462b679e..cd5e80fe9ae92f 100644 --- a/src/vs/workbench/contrib/update/browser/postUpdateWidget.ts +++ b/src/vs/workbench/contrib/update/browser/postUpdateWidget.ts @@ -75,7 +75,8 @@ export class PostUpdateWidgetContribution extends Disposable implements IWorkben return; } - if (this.meteredConnectionService.isConnectionMetered) { + await this.meteredConnectionService.whenInitialized; + if (this._store.isDisposed || this.meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/update/test/electron-browser/postUpdateWidget.test.ts b/src/vs/workbench/contrib/update/test/electron-browser/postUpdateWidget.test.ts index e29834c8f9d18b..bf9621d8e21641 100644 --- a/src/vs/workbench/contrib/update/test/electron-browser/postUpdateWidget.test.ts +++ b/src/vs/workbench/contrib/update/test/electron-browser/postUpdateWidget.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { timeout } from '../../../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { bufferToStream, VSBuffer } from '../../../../../base/common/buffer.js'; import { IRequestContext } from '../../../../../base/parts/request/common/request.js'; import { mock } from '../../../../../base/test/common/mock.js'; @@ -38,7 +38,7 @@ class TestRequestService extends mock() { suite('PostUpdateWidgetContribution (Electron)', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); - function createContribution(isConnectionMetered: boolean): TestRequestService { + function createContribution(isConnectionMetered: boolean, whenInitialized: Promise = Promise.resolve()): TestRequestService { const requestService = new TestRequestService(); const configurationService = new TestConfigurationService(); store.add(configurationService.onDidChangeConfigurationEmitter); @@ -55,6 +55,7 @@ suite('PostUpdateWidgetContribution (Electron)', () => { new class extends mock() { }, new class extends mock() { override readonly isConnectionMetered = isConnectionMetered; + override readonly whenInitialized = whenInitialized; }, new class extends mock() { }, new class extends mock() { @@ -81,6 +82,19 @@ suite('PostUpdateWidgetContribution (Electron)', () => { assert.strictEqual(requestService.requestCount, 1); }); + test('waits for metered connection initialization before requesting update info automatically', async () => { + const initialized = new DeferredPromise(); + const requestService = createContribution(false, initialized.p); + + await timeout(0); + assert.strictEqual(requestService.requestCount, 0); + + initialized.complete(); + await timeout(0); + + assert.strictEqual(requestService.requestCount, 1); + }); + test('skips the automatic request while metered but preserves the explicit command', async () => { const requestService = createContribution(true); diff --git a/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts b/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts index 62051fe8b04657..b764067c0d8811 100644 --- a/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts +++ b/src/vscode-dts/vscode.proposed.envIsConnectionMetered.d.ts @@ -9,6 +9,8 @@ declare module 'vscode' { /** * Whether the current network connection is metered (such as mobile data or tethering). * Always returns `false` if the `network.meteredConnection` setting is set to `off`. + * May conservatively return `true` during extension host initialization before the operating system state is + * available. {@link onDidChangeMeteredConnection} fires if initialization corrects the value. */ export const isMeteredConnection: boolean; From 453e0ca8e7433a7191c53e6c27eaf1c94eb5a201 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 2 Sep 2026 19:55:32 -0700 Subject: [PATCH 03/11] meteredConnection: update package integrity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1beb101810a8a1..ffc47da5ebcb22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4788,7 +4788,7 @@ "node_modules/@vscode/metered": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@vscode/metered/-/metered-0.1.0.tgz", - "integrity": "sha512-u8wPCGycvpzADInvEGFZgDBR/d7EQ1U+rxwFIoUGmOMk7BxbeQzYGRLIXhgcczOPQnL8fjJzTddArB0gjsHWNQ==", + "integrity": "sha512-+vbjI2p2tmiw16S1hWIMDLNi7PU+4KyIhe5gAm+pilwb5hb0HcqtpWBGQumSZKx2UFXoQ9ix6EHvcIL4O/5d/w==", "hasInstallScript": true, "license": "MIT", "dependencies": { From 08f2db31434f3c44c1037398f4d408ad26177cb0 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 2 Sep 2026 20:48:50 -0700 Subject: [PATCH 04/11] meteredConnection: fix Linux builds and lifecycle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 1 + .github/workflows/monaco-editor.yml | 2 +- .github/workflows/pr-linux-test.yml | 1 + .../environment/test/node/nativeModules.integrationTest.ts | 1 + .../contrib/extensions/browser/extensionsWorkbenchService.ts | 5 ++++- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 3daeb39c938ee0..e076a86e3b428a 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -41,6 +41,7 @@ jobs: ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ xvfb \ libgtk-3-0 \ + libglib2.0-dev \ libxkbfile-dev \ libkrb5-dev \ libgbm1 \ diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index a3653ee3621663..5f24c5e004b450 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -53,7 +53,7 @@ jobs: if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} run: | sudo apt update - sudo apt install -y libxkbfile-dev pkg-config libkrb5-dev libxss1 + sudo apt install -y libglib2.0-dev libxkbfile-dev pkg-config libkrb5-dev libxss1 - name: Execute npm if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} env: diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 1eb53e5de9f00f..3d09ada610cd4e 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -60,6 +60,7 @@ jobs: ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ xvfb \ libgtk-3-0 \ + libglib2.0-dev \ libxkbfile-dev \ libkrb5-dev \ libgbm1 \ diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 0c156bacfad9fd..7ae10996aa30a9 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -68,6 +68,7 @@ flakySuite('Native Modules (all platforms)', () => { test('@vscode/metered', async () => { const metered = await import('@vscode/metered'); assert.ok(typeof metered.createMonitor === 'function', testErrorMessage('@vscode/metered')); + metered.createMonitor().dispose(); }); test('node-pty', async () => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index a242e36179c4d6..0a964843e10fcc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2254,7 +2254,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async autoUpdateBuiltinExtensions(): Promise { await this.meteredConnectionService.whenInitialized; - if (this.meteredConnectionService.isConnectionMetered) { + if (this._store.isDisposed || this.meteredConnectionService.isConnectionMetered) { return; } await this.checkForUpdates(undefined, true); @@ -2279,6 +2279,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async autoUpdateExtensions(): Promise { await this.meteredConnectionService.whenInitialized; + if (this._store.isDisposed) { + return; + } if (this.meteredConnectionService.isConnectionMetered) { this.logService.trace('[Extensions]: Skipping auto-update because connection is metered'); return; From 759131984d8be81d488ec703e4fc3c365362ee2b Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 2 Sep 2026 23:00:43 -0700 Subject: [PATCH 05/11] meteredConnection: prepare Linux build dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .devcontainer/install-vscode.sh | 2 +- .github/workflows/chat-perf.yml | 6 +++--- .github/workflows/pr-node-modules.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/sessions-e2e.yml | 2 +- .../alpine/product-build-alpine-node-modules.yml | 2 +- .../alpine/product-build-alpine.yml | 2 +- build/azure-pipelines/linux/setup-env.sh | 16 ++++------------ .../web/product-build-web-node-modules.yml | 2 +- build/azure-pipelines/web/product-build-web.yml | 2 +- build/linux/debian/install-sysroot.ts | 11 ++++++----- src/vs/code/electron-main/app.ts | 2 +- 12 files changed, 22 insertions(+), 29 deletions(-) diff --git a/.devcontainer/install-vscode.sh b/.devcontainer/install-vscode.sh index cc70d527acdfba..ee3dfd52d19f8d 100755 --- a/.devcontainer/install-vscode.sh +++ b/.devcontainer/install-vscode.sh @@ -9,4 +9,4 @@ sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.mi rm -f packages.microsoft.gpg apt update -apt install -y code-insiders libsecret-1-dev libxkbfile-dev libkrb5-dev +apt install -y code-insiders libglib2.0-dev libsecret-1-dev libxkbfile-dev libkrb5-dev diff --git a/.github/workflows/chat-perf.yml b/.github/workflows/chat-perf.yml index ba94b89cf1965c..138881f65d1758 100644 --- a/.github/workflows/chat-perf.yml +++ b/.github/workflows/chat-perf.yml @@ -109,7 +109,7 @@ jobs: sudo apt install -y \ build-essential pkg-config \ libx11-dev libx11-xcb-dev libxkbfile-dev \ - libnotify-bin libkrb5-dev \ + libnotify-bin libkrb5-dev libglib2.0-dev \ xvfb sqlite3 \ libnss3 libatk1.0-0 libatk-bridge2.0-0 \ libcups2t64 libdrm2 libxcomposite1 libxdamage1 \ @@ -228,7 +228,7 @@ jobs: sudo apt install -y \ build-essential pkg-config \ libx11-dev libx11-xcb-dev libxkbfile-dev \ - libnotify-bin libkrb5-dev \ + libnotify-bin libkrb5-dev libglib2.0-dev \ xvfb sqlite3 \ libnss3 libatk1.0-0 libatk-bridge2.0-0 \ libcups2t64 libdrm2 libxcomposite1 libxdamage1 \ @@ -410,7 +410,7 @@ jobs: sudo apt install -y \ build-essential pkg-config \ libx11-dev libx11-xcb-dev libxkbfile-dev \ - libnotify-bin libkrb5-dev \ + libnotify-bin libkrb5-dev libglib2.0-dev \ xvfb \ libnss3 libatk1.0-0 libatk-bridge2.0-0 \ libcups2t64 libdrm2 libxcomposite1 libxdamage1 \ diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 8b83a03492fe79..a718b9972979f4 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -29,7 +29,7 @@ jobs: - name: Install build tools if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev + run: sudo apt update -y && sudo apt install -y build-essential pkg-config libglib2.0-dev libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f196afce554fb7..f9748e7b5e5c9c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -41,7 +41,7 @@ jobs: - name: Install build tools if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev + run: sudo apt update -y && sudo apt install -y build-essential pkg-config libglib2.0-dev libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev - name: Install dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' diff --git a/.github/workflows/sessions-e2e.yml b/.github/workflows/sessions-e2e.yml index 16371d093eb4a2..b61bcc03178654 100644 --- a/.github/workflows/sessions-e2e.yml +++ b/.github/workflows/sessions-e2e.yml @@ -33,7 +33,7 @@ jobs: node-version-file: .nvmrc - name: Install build tools - run: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev xvfb + run: sudo apt update -y && sudo apt install -y build-essential pkg-config libglib2.0-dev libx11-dev libx11-xcb-dev libxkbfile-dev libnotify-bin libkrb5-dev xvfb - name: Install dependencies run: npm ci diff --git a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml index a7cc0ab6f3d20b..aebcbaf0a5696d 100644 --- a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml +++ b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml @@ -86,7 +86,7 @@ jobs: displayName: "Pull qemu-user-static image" condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['NPM_ARCH'], 'arm64')) - - script: sudo apt-get update && sudo apt-get install -y libkrb5-dev + - script: sudo apt-get update && sudo apt-get install -y libglib2.0-dev libkrb5-dev displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 6c72b85a219947..1860dae08c2305 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -130,7 +130,7 @@ jobs: displayName: "Pull qemu-user-static image" condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['NPM_ARCH'], 'arm64')) - - script: sudo apt-get update && sudo apt-get install -y libkrb5-dev + - script: sudo apt-get update && sudo apt-get install -y libglib2.0-dev libkrb5-dev displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index df123593b1ab49..96e7ef59521c18 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -9,19 +9,11 @@ fi export VSCODE_CLIENT_SYSROOT_DIR=$PWD/.build/sysroots/glibc-2.28-gcc-10.5.0 export VSCODE_REMOTE_SYSROOT_DIR=$PWD/.build/sysroots/glibc-2.28-gcc-8.5.0 -if [ -d "$VSCODE_CLIENT_SYSROOT_DIR" ]; then - echo "Using cached client sysroot" -else - echo "Downloading client sysroot" - SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT_DIR" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' -fi +echo "Ensuring client sysroot" +SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT_DIR" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' -if [ -d "$VSCODE_REMOTE_SYSROOT_DIR" ]; then - echo "Using cached remote sysroot" -else - echo "Downloading remote sysroot" - SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_REMOTE_SYSROOT_DIR" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' -fi +echo "Ensuring remote sysroot" +SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_REMOTE_SYSROOT_DIR" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' mkdir -p "$HOME/.gyp" cat > "$HOME/.gyp/include.gypi" << 'EOF' diff --git a/build/azure-pipelines/web/product-build-web-node-modules.yml b/build/azure-pipelines/web/product-build-web-node-modules.yml index 4f935de7336480..d7d8b71dc2b68a 100644 --- a/build/azure-pipelines/web/product-build-web-node-modules.yml +++ b/build/azure-pipelines/web/product-build-web-node-modules.yml @@ -62,7 +62,7 @@ jobs: - script: | set -e ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update - ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y libkrb5-dev + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y libglib2.0-dev libkrb5-dev displayName: Setup system services condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 6a6c132da177f0..4ef24f4b8fd5b7 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -75,7 +75,7 @@ jobs: - script: | set -e ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update - ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y libkrb5-dev + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y libglib2.0-dev libkrb5-dev displayName: Setup system services condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts index 28cd4e85c6a2dc..8210a26e0d5bdb 100644 --- a/build/linux/debian/install-sysroot.ts +++ b/build/linux/debian/install-sysroot.ts @@ -17,6 +17,7 @@ import type { DebianArchString } from './types.ts'; const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; const URL_PATH = 'sysroots/toolchain'; const REPO_ROOT = path.dirname(path.dirname(path.dirname(import.meta.dirname))); +const VSCODE_SYSROOT_VERSION = '20260212-405735'; const ghApiHeaders: Record = { Accept: 'application/vnd.github.v3+json', @@ -70,8 +71,7 @@ function getVSCodeSysrootChecksum(expectedName: string) { * tar implementation for that reason. */ async function fetchUrl(options: IFetchOptions): Promise { - const version = '20260212-405735'; - const releaseUrl = `https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${version}`; + const releaseUrl = `https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${VSCODE_SYSROOT_VERSION}`; const downloadOptions = { attempts: 11, onRetry: (error: Error) => console.log(`Fetching failed: ${error}`) @@ -79,7 +79,7 @@ async function fetchUrl(options: IFetchOptions): Promise { const releaseContents = await download(releaseUrl, { ...downloadOptions, headers: ghApiHeaders }); const asset = JSON.parse(Buffer.from(releaseContents).toString()).assets.find((a: { name: string }) => a.name === options.assetName); if (!asset) { - throw new Error(`Could not find asset in release of Microsoft/vscode-linux-build-agent @ ${version}`); + throw new Error(`Could not find asset in release of Microsoft/vscode-linux-build-agent @ ${VSCODE_SYSROOT_VERSION}`); } console.log(`Found asset ${options.assetName} @ ${asset.url}.`); @@ -130,11 +130,12 @@ export async function getVSCodeSysroot(arch: DebianArchString, isMusl: boolean = } const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path.join(tmpdir(), `vscode-${arch}-sysroot`); const stamp = path.join(sysroot, '.stamp'); + const expectedStamp = `${VSCODE_SYSROOT_VERSION}/${expectedName}`; let result = `${sysroot}/${triple}/${triple}/sysroot`; if (isMusl) { result = `${sysroot}/output/${triple}`; } - if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedName) { + if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedStamp) { return result; } console.log(`Installing ${arch} root image: ${sysroot}`); @@ -145,7 +146,7 @@ export async function getVSCodeSysroot(arch: DebianArchString, isMusl: boolean = assetName: expectedName, dest: sysroot }); - fs.writeFileSync(stamp, expectedName); + fs.writeFileSync(stamp, expectedStamp); return result; } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index a0e45c3e07418b..52a721e5968b9d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1232,7 +1232,7 @@ export class CodeApplication extends Disposable { services.set(IGlobalKeybindingsMainService, new SyncDescriptor(GlobalKeybindingsMainService, [globalShortcut])); // Metered Connection - const meteredConnectionService = new MeteredConnectionMainService(undefined, this.configurationService, this.logService); + const meteredConnectionService = this._register(new MeteredConnectionMainService(undefined, this.configurationService, this.logService)); services.set(IMeteredConnectionService, meteredConnectionService); // Web Contents Extractor From 79d751259cfc9c6357a19e48ad2a77bc8bc9d8d4 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Sun, 6 Sep 2026 23:02:13 -0700 Subject: [PATCH 06/11] build: replace Debian 11 sanity coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../product-build-template.yml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/build/azure-pipelines/product-build-template.yml b/build/azure-pipelines/product-build-template.yml index 4107898579dfed..29783cb7560c1c 100644 --- a/build/azure-pipelines/product-build-template.yml +++ b/build/azure-pipelines/product-build-template.yml @@ -413,32 +413,32 @@ stages: container: centos arch: arm64 - # Debian 11 + # Debian 13 - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - template: common/sanity-tests.yml@self parameters: - name: debian_11_amd64 - displayName: Debian 11 amd64 + name: debian_13_amd64 + displayName: Debian 13 amd64 poolName: 1es-ubuntu-22.04-x64 - container: debian-11 + container: debian-13 arch: amd64 - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}: - template: common/sanity-tests.yml@self parameters: - name: debian_11_arm32 - displayName: Debian 11 arm32 + name: debian_13_arm32 + displayName: Debian 13 arm32 poolName: 1es-azure-linux-3-arm64 - container: debian-11 + container: debian-13 arch: arm - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}: - template: common/sanity-tests.yml@self parameters: - name: debian_11_arm64 - displayName: Debian 11 arm64 + name: debian_13_arm64 + displayName: Debian 13 arm64 poolName: 1es-azure-linux-3-arm64 - container: debian-11 + container: debian-13 arch: arm64 # Debian 12 From 7606f5544019ccf632e76b101d10330a05afaa07 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 8 Sep 2026 12:02:03 -0700 Subject: [PATCH 07/11] build: use GLib-enabled Linux sysroots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/azure-pipelines/linux/setup-env.sh | 19 ++++++++++++++++++ .../product-build-template.yml | 20 +++++++++---------- build/checksums/vscode-sysroot.txt | 14 ++++++------- build/linux/debian/install-sysroot.ts | 2 +- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 96e7ef59521c18..b1fb3156b0004e 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -15,6 +15,25 @@ SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT_DIR" nod echo "Ensuring remote sysroot" SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_REMOTE_SYSROOT_DIR" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' +if [ "$npm_config_arch" == "x64" ]; then + VSCODE_CLIENT_TOOLCHAIN_TRIPLE="x86_64-linux-gnu" + VSCODE_CLIENT_LIBRARY_TRIPLE="x86_64-linux-gnu" +elif [ "$npm_config_arch" == "arm64" ]; then + VSCODE_CLIENT_TOOLCHAIN_TRIPLE="aarch64-linux-gnu" + VSCODE_CLIENT_LIBRARY_TRIPLE="aarch64-linux-gnu" +elif [ "$npm_config_arch" == "arm" ]; then + VSCODE_CLIENT_TOOLCHAIN_TRIPLE="arm-rpi-linux-gnueabihf" + VSCODE_CLIENT_LIBRARY_TRIPLE="arm-linux-gnueabihf" +else + echo "Unsupported npm architecture: $npm_config_arch" >&2 + exit 1 +fi + +VSCODE_CLIENT_SYSROOT="$VSCODE_CLIENT_SYSROOT_DIR/$VSCODE_CLIENT_TOOLCHAIN_TRIPLE/$VSCODE_CLIENT_TOOLCHAIN_TRIPLE/sysroot" +export PKG_CONFIG_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT" +export PKG_CONFIG_LIBDIR="$VSCODE_CLIENT_SYSROOT/usr/lib/$VSCODE_CLIENT_LIBRARY_TRIPLE/pkgconfig:$VSCODE_CLIENT_SYSROOT/usr/lib/pkgconfig:$VSCODE_CLIENT_SYSROOT/usr/share/pkgconfig" +unset PKG_CONFIG_PATH + mkdir -p "$HOME/.gyp" cat > "$HOME/.gyp/include.gypi" << 'EOF' { diff --git a/build/azure-pipelines/product-build-template.yml b/build/azure-pipelines/product-build-template.yml index 29783cb7560c1c..4107898579dfed 100644 --- a/build/azure-pipelines/product-build-template.yml +++ b/build/azure-pipelines/product-build-template.yml @@ -413,32 +413,32 @@ stages: container: centos arch: arm64 - # Debian 13 + # Debian 11 - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - template: common/sanity-tests.yml@self parameters: - name: debian_13_amd64 - displayName: Debian 13 amd64 + name: debian_11_amd64 + displayName: Debian 11 amd64 poolName: 1es-ubuntu-22.04-x64 - container: debian-13 + container: debian-11 arch: amd64 - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}: - template: common/sanity-tests.yml@self parameters: - name: debian_13_arm32 - displayName: Debian 13 arm32 + name: debian_11_arm32 + displayName: Debian 11 arm32 poolName: 1es-azure-linux-3-arm64 - container: debian-13 + container: debian-11 arch: arm - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}: - template: common/sanity-tests.yml@self parameters: - name: debian_13_arm64 - displayName: Debian 13 arm64 + name: debian_11_arm64 + displayName: Debian 11 arm64 poolName: 1es-azure-linux-3-arm64 - container: debian-13 + container: debian-11 arch: arm64 # Debian 12 diff --git a/build/checksums/vscode-sysroot.txt b/build/checksums/vscode-sysroot.txt index 847383e7a1a397..bd736ea5ec0d56 100644 --- a/build/checksums/vscode-sysroot.txt +++ b/build/checksums/vscode-sysroot.txt @@ -1,7 +1,7 @@ -38fa5acc4e4f17cb7e3f599b5226a511b22cbdceeb38277d224b45f7280f518f aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz -bcff71257397f76a4f7a76bbcfc425b4b60b16449e10b01a2cfce574c21d60da aarch64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz -58cd59ee4038291fe8a7f4adccac0ecbe8d23cbad1cb650b381e45e7e1e22424 aarch64-linux-musl-gcc-10.3.0.tar.gz -a0a1573e93191ae5e3735eb6107f4daeb4be7f99c5af229114d990ac77db33eb arm-rpi-linux-gnueabihf-glibc-2.28-gcc-10.5.0.tar.gz -375ecbb95c7eed95d6c7918af3c4418f2907e92e2f8d1b827a65bf46a8bbfc74 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-8.5.0.tar.gz -ac4b6b14b4cec027a22a51bbbb049b3504958a78106c8a8d5cec144206b767d1 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz -1ebb6ef1fe2983269fd0855a88f9c9a37f9b515d16524a9146198e4cabdf34f7 x86_64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz +3a87312e8055cd3ad27a1da8ac75e65cbea51286dd6449b841d0b11e61d7cb46 aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +96fe325649bcdf258c75f5910166194f4fd16f38a17375864a5c39468372f27c aarch64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz +2f3375724226a0868e417fe7c87e30e30d87c0bba2e944826e8d12b2bc51e3fe aarch64-linux-musl-gcc-10.3.0.tar.gz +60e716aa9e102b5a0c2bc980eaf34e07674a541ee65e95e53d512f5d2b8cdb01 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-10.5.0.tar.gz +7c68a8a910ded3255e382e101d417c8fb569f5e802b40b74302138459d89b256 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-8.5.0.tar.gz +836d2379285e1ab942e43a7772a00dc8da4ffd823f4866031ea5c0f557877cf6 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +b413d7e26ebbdd59ce73570dfed2b045fdf8ffc81765933e6f7ca41e7a09f339 x86_64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts index 8210a26e0d5bdb..feae519ecba9d7 100644 --- a/build/linux/debian/install-sysroot.ts +++ b/build/linux/debian/install-sysroot.ts @@ -17,7 +17,7 @@ import type { DebianArchString } from './types.ts'; const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; const URL_PATH = 'sysroots/toolchain'; const REPO_ROOT = path.dirname(path.dirname(path.dirname(import.meta.dirname))); -const VSCODE_SYSROOT_VERSION = '20260212-405735'; +const VSCODE_SYSROOT_VERSION = '20260908-471715'; const ghApiHeaders: Record = { Accept: 'application/vnd.github.v3+json', From 5552257aed8a9003aa79243dfb3626ca5328785c Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 8 Sep 2026 23:51:05 -0700 Subject: [PATCH 08/11] build: use complete GLib Linux sysroots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/checksums/vscode-sysroot.txt | 14 +++++++------- build/linux/debian/install-sysroot.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/checksums/vscode-sysroot.txt b/build/checksums/vscode-sysroot.txt index bd736ea5ec0d56..60e905648d86a1 100644 --- a/build/checksums/vscode-sysroot.txt +++ b/build/checksums/vscode-sysroot.txt @@ -1,7 +1,7 @@ -3a87312e8055cd3ad27a1da8ac75e65cbea51286dd6449b841d0b11e61d7cb46 aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz -96fe325649bcdf258c75f5910166194f4fd16f38a17375864a5c39468372f27c aarch64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz -2f3375724226a0868e417fe7c87e30e30d87c0bba2e944826e8d12b2bc51e3fe aarch64-linux-musl-gcc-10.3.0.tar.gz -60e716aa9e102b5a0c2bc980eaf34e07674a541ee65e95e53d512f5d2b8cdb01 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-10.5.0.tar.gz -7c68a8a910ded3255e382e101d417c8fb569f5e802b40b74302138459d89b256 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-8.5.0.tar.gz -836d2379285e1ab942e43a7772a00dc8da4ffd823f4866031ea5c0f557877cf6 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz -b413d7e26ebbdd59ce73570dfed2b045fdf8ffc81765933e6f7ca41e7a09f339 x86_64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz +101468da438ec4cdbc77cfc03f9c97a1b397a7eefa7a4ab15aaaa17607b7462e aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +31e0d57ea86232f577295a57987cfc1d3c96e295094be4e93f8322a413be1a7f aarch64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz +03cbb3f523d6dcff9d5eaafb12e86389b8f712ce4e324a357d9ec09be921603d aarch64-linux-musl-gcc-10.3.0.tar.gz +eadc69f945fc71c9455c15b57a47b84b7e8a7b592ef03c9805a7c482ea9e4570 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-10.5.0.tar.gz +3314fe2f81f0b849647ec111c776c80bc33ca199444714871c1d44ac95010c3d arm-rpi-linux-gnueabihf-glibc-2.28-gcc-8.5.0.tar.gz +c4719c3944276de3051e3200d92c162e95cab79665f11d023b1f393263cf2e49 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +6546ca42b458b8655957e701e2d02a8536bc11544efe61da3bc5f82fc09e0e7d x86_64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts index feae519ecba9d7..be3c6e8f634e13 100644 --- a/build/linux/debian/install-sysroot.ts +++ b/build/linux/debian/install-sysroot.ts @@ -17,7 +17,7 @@ import type { DebianArchString } from './types.ts'; const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; const URL_PATH = 'sysroots/toolchain'; const REPO_ROOT = path.dirname(path.dirname(path.dirname(import.meta.dirname))); -const VSCODE_SYSROOT_VERSION = '20260908-471715'; +const VSCODE_SYSROOT_VERSION = '20260909-472333'; const ghApiHeaders: Record = { Accept: 'application/vnd.github.v3+json', From cee55617167eaf54b51282077e0ed5914a488ade Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 9 Sep 2026 18:05:08 -0700 Subject: [PATCH 09/11] build: update Linux package dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/linux/debian/dep-lists.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index eb8b42624b2852..2f922b333b253a 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -43,6 +43,7 @@ export const referenceGeneratedDepsByArch = { 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.12.0)', + 'libglib2.0-0 (>= 2.31.8)', 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', @@ -81,6 +82,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', + 'libglib2.0-0 (>= 2.31.8)', 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', @@ -121,6 +123,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', + 'libglib2.0-0 (>= 2.31.8)', 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', From 2c447a8c70b3001d130ed5951a13f63970acd66a Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Sun, 20 Sep 2026 08:07:03 -0700 Subject: [PATCH 10/11] metered: isolate native detection from consumer integrations Keep native detection, packaging, initialization, defaults and update handling in the primary PR. Move telemetry, Settings Sync, plugin updates and extension auto-updates to separate stacked changes. Temporarily remove existing telemetry metered gating and state-change instrumentation so the primary branch is independent of the telemetry integration. Default auto-detection to Insiders only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/code/electron-main/app.ts | 6 +- .../sharedProcess/sharedProcessMain.ts | 1 - .../meteredConnection.config.contribution.ts | 3 +- .../meteredConnectionMainService.ts | 30 ------- .../meteredConnectionMainService.test.ts | 8 -- .../telemetry/common/telemetryService.ts | 57 +++----------- .../test/browser/telemetryService.test.ts | 78 +------------------ .../common/userDataAutoSyncService.ts | 13 +--- .../test/common/userDataSyncClient.ts | 2 +- .../contrib/chat/browser/pluginAutoUpdate.ts | 1 - .../plugins/pluginMarketplaceService.ts | 14 +--- .../browser/plugins/pluginAutoUpdate.test.ts | 24 +----- .../plugins/pluginMarketplaceService.test.ts | 36 --------- .../browser/extensionsWorkbenchService.ts | 42 ++++------ .../extensionRecommendationsService.test.ts | 2 +- .../extensionsActions.test.ts | 2 +- .../electron-browser/extensionsViews.test.ts | 2 +- .../extensionsWorkbenchService.test.ts | 28 +------ .../telemetry/browser/telemetryService.ts | 10 +-- 19 files changed, 44 insertions(+), 315 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index b56a644c39c6ea..ea1cbd86b3e953 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -755,11 +755,9 @@ export class CodeApplication extends Disposable { // This manager self-disposes after its lifecycle join; CodeApplication disposes before later shutdown listeners run. appInstantiationService.createInstance(AgentHostProcessManager, agentHostStarter, process.platform); - // Metered connection telemetry + // Metered Connection appInstantiationService.invokeFunction(accessor => { - const meteredConnectionService = accessor.get(IMeteredConnectionService) as MeteredConnectionMainService; - meteredConnectionService.setTelemetryService(accessor.get(ITelemetryService)); - meteredConnectionService.start(); + (accessor.get(IMeteredConnectionService) as MeteredConnectionMainService).start(); }); // Auth Handler diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index d32bf32badc887..d559985d182fb2 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -343,7 +343,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.devDeviceId, internalTelemetry, productService.date), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), - meteredConnectionService, }, configurationService, productService); } else { telemetryService = NullTelemetryService; diff --git a/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts b/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts index 1259f403a12ecf..67e7aff827355f 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts @@ -5,6 +5,7 @@ import { localize } from '../../../nls.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; +import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -22,7 +23,7 @@ configurationRegistry.registerConfiguration({ localize('meteredConnection.on', "Always treat the network connection as metered. Automatic updates and downloads will be postponed."), localize('meteredConnection.off', "Never treat the network connection as metered.") ], - default: 'auto', + default: product.quality === 'insider' ? 'auto' : 'off', scope: ConfigurationScope.APPLICATION, description: localize('meteredConnection', "Controls whether the current network connection should be treated as metered. When metered, automatic updates, extension downloads, and other background network activity will be postponed to reduce data usage."), tags: ['usesOnlineServices'] diff --git a/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts b/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts index 1b6f724baedb65..731e49c0145654 100644 --- a/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts +++ b/src/vs/platform/meteredConnection/electron-main/meteredConnectionMainService.ts @@ -7,7 +7,6 @@ import type { MeteredConnectionMonitor, MeteredConnectionState } from '@vscode/m import { DeferredPromise, raceTimeout } from '../../../base/common/async.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ILogService } from '../../log/common/log.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AbstractMeteredConnectionService } from '../common/meteredConnection.js'; type MonitorFactory = () => Promise; @@ -28,7 +27,6 @@ async function createMonitor(): Promise { * This implementation receives metered connection updates from the operating system. */ export class MeteredConnectionMainService extends AbstractMeteredConnectionService { - private telemetryService: ITelemetryService | undefined; private readonly monitorFactory: MonitorFactory; private readonly initialized = new DeferredPromise(); private readonly initializationTimeout: number; @@ -45,10 +43,6 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi this.initializationTimeout = options?.initializationTimeout ?? INITIALIZATION_TIMEOUT; } - public setTelemetryService(telemetryService: ITelemetryService): void { - this.telemetryService = telemetryService; - } - public start(): void { if (!this.started) { this.started = true; @@ -106,28 +100,4 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi return false; } } - - protected override onChangeUnderlyingConnection() { - // Fire event after sending telemetry if switching to metered since telemetry will be paused. - const fireAfter = this.isUnderlyingConnectionMetered; - if (!fireAfter) { - super.onChangeUnderlyingConnection(); - } - - type MeteredConnectionStateChangeEvent = { - connectionState: boolean; - }; - type MeteredConnectionStateChangeClassification = { - owner: 'dmitrivMS'; - comment: 'Tracks metered network connection state changes to understand usage patterns.'; - connectionState: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the underlying network connection is metered according to the OS.' }; - }; - this.telemetryService?.publicLog2('meteredConnectionStateChange', { - connectionState: this.isUnderlyingConnectionMetered, - }); - - if (fireAfter) { - super.onChangeUnderlyingConnection(); - } - } } diff --git a/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts b/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts index 9a168b420ff9c9..ba6bac5a95fc73 100644 --- a/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts +++ b/src/vs/platform/meteredConnection/test/electron-main/meteredConnectionMainService.test.ts @@ -10,7 +10,6 @@ import { Emitter } from '../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { NullLogService } from '../../../log/common/log.js'; -import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; import { MeteredConnectionCommand } from '../../common/meteredConnectionIpc.js'; import { MeteredConnectionChannel } from '../../electron-main/meteredConnectionChannel.js'; import { MeteredConnectionMainService } from '../../electron-main/meteredConnectionMainService.js'; @@ -47,7 +46,6 @@ suite('MeteredConnectionMainService', () => { store.add(configurationService.onDidChangeConfigurationEmitter); const monitor = new TestMeteredConnectionMonitor(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); let initialized = false; void service.whenInitialized.then(() => initialized = true); @@ -75,7 +73,6 @@ suite('MeteredConnectionMainService', () => { store.add(configurationService.onDidChangeConfigurationEmitter); const monitor = new TestMeteredConnectionMonitor(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); const changes: boolean[] = []; store.add(service.onDidChangeIsConnectionMetered(state => changes.push(state))); @@ -111,7 +108,6 @@ suite('MeteredConnectionMainService', () => { store.add(configurationService.onDidChangeConfigurationEmitter); const monitor = new TestMeteredConnectionMonitor(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); const channel = new MeteredConnectionChannel(service); let resolved = false; @@ -139,7 +135,6 @@ suite('MeteredConnectionMainService', () => { monitorFactory: async () => monitor, initializationTimeout: 0, }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); await service.whenInitialized; @@ -163,7 +158,6 @@ suite('MeteredConnectionMainService', () => { monitorFactory: () => monitorPromise.p, initializationTimeout: 0, }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); await service.whenInitialized; @@ -182,7 +176,6 @@ suite('MeteredConnectionMainService', () => { store.add(configurationService.onDidChangeConfigurationEmitter); const monitor = new TestMeteredConnectionMonitor(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); await timeout(0); @@ -206,7 +199,6 @@ suite('MeteredConnectionMainService', () => { const monitor = new TestMeteredConnectionMonitor(); const monitorPromise = new DeferredPromise(); const service = store.add(new MeteredConnectionMainService({ monitorFactory: () => monitorPromise.p }, configurationService, new NullLogService())); - service.setTelemetryService(NullTelemetryService); service.start(); service.dispose(); diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 90b98e2aa72b75..4e472aec13a00f 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -11,7 +11,6 @@ import { escapeRegExpCharacters } from '../../../base/common/strings.js'; import { localize } from '../../../nls.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; -import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; import product from '../../product/common/product.js'; import { IProductService } from '../../product/common/productService.js'; import { Registry } from '../../registry/common/platform.js'; @@ -34,10 +33,6 @@ export interface ITelemetryServiceConfig { * (up to 10 seconds) to ensure experiment context is attached to all events. */ waitForExperimentProperties?: boolean; - /** - * If provided, telemetry events will be dropped when the connection is metered. - */ - meteredConnectionService?: IMeteredConnectionService; } interface IPendingEvent { @@ -69,13 +64,9 @@ export class TelemetryService implements ITelemetryService { private _telemetryLevel: TelemetryLevel; private _sendErrorTelemetry: boolean; - private readonly _meteredConnectionService: IMeteredConnectionService | undefined; - private _isMeteredConnectionInitialized: boolean; - private _pendingEvents: IPendingEvent[] = []; private _isExperimentPropertySet = false; private _flushTimeout: ReturnType | undefined; - private _isDisposed = false; private readonly _disposables = new DisposableStore(); private _cleanupPatterns: RegExp[] = []; @@ -103,16 +94,6 @@ export class TelemetryService implements ITelemetryService { this._piiPaths = config.piiPaths || []; this._telemetryLevel = TelemetryLevel.USAGE; this._sendErrorTelemetry = !!config.sendErrorTelemetry; - this._meteredConnectionService = config.meteredConnectionService; - this._isMeteredConnectionInitialized = !this._meteredConnectionService; - if (this._meteredConnectionService) { - void this._meteredConnectionService.whenInitialized.then(() => { - if (!this._isDisposed) { - this._isMeteredConnectionInitialized = true; - this._flushPendingEventsIfReady(); - } - }); - } // static cleanup pattern for: `vscode-file:///DANGEROUS/PATH/resources/app/Useful/Information` this._cleanupPatterns = [/(vscode-)?file:\/\/.*?\/resources\/app\//gi]; @@ -170,29 +151,19 @@ export class TelemetryService implements ITelemetryService { } } - private _flushPendingEvents(force = false): void { - if (!this._isExperimentPropertySet) { - this._isExperimentPropertySet = true; - - if (this._flushTimeout !== undefined) { - clearTimeout(this._flushTimeout); - this._flushTimeout = undefined; - } - } - - this._flushPendingEventsIfReady(force); - } - - private _flushPendingEventsIfReady(force = false): void { - if (!this._isExperimentPropertySet || (!this._isMeteredConnectionInitialized && !force)) { + private _flushPendingEvents(): void { + if (this._isExperimentPropertySet) { return; } - if (this._meteredConnectionService?.isConnectionMetered) { - this._pendingEvents = []; - return; + this._isExperimentPropertySet = true; + + if (this._flushTimeout !== undefined) { + clearTimeout(this._flushTimeout); + this._flushTimeout = undefined; } + // Send all buffered events now that experiment properties are available for (const event of this._pendingEvents) { this._doLog(event.eventName, event.eventLevel, event.data); } @@ -226,9 +197,8 @@ export class TelemetryService implements ITelemetryService { } dispose(): void { - this._isDisposed = true; // Flush any remaining pending events before disposing - this._flushPendingEvents(true); + this._flushPendingEvents(); this._disposables.dispose(); } @@ -238,19 +208,14 @@ export class TelemetryService implements ITelemetryService { return; } - // Buffer events until experiment properties and the initial metered connection state are available. - if (!this._isExperimentPropertySet || !this._isMeteredConnectionInitialized) { + // Buffer events until experiment properties are set (or timeout expires) + if (!this._isExperimentPropertySet) { if (this._pendingEvents.length < TelemetryService.MAX_BUFFER_SIZE) { this._pendingEvents.push({ eventName, eventLevel, data }); } return; } - // Don't send events when the connection is metered - if (this._meteredConnectionService?.isConnectionMetered) { - return; - } - this._doLog(eventName, eventLevel, data); } diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 6df5b825343e00..1447eddaaef6ee 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -6,12 +6,10 @@ import assert from 'assert'; import * as sinon from 'sinon'; import sinonTest from 'sinon-test'; import { mainWindow } from '../../../../base/browser/window.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; import * as Errors from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Emitter } from '../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; -import { IMeteredConnectionService } from '../../../meteredConnection/common/meteredConnection.js'; import product from '../../../product/common/product.js'; import { IProductService } from '../../../product/common/productService.js'; import ErrorTelemetry from '../../browser/errorTelemetry.js'; @@ -45,16 +43,6 @@ class TestTelemetryAppender implements ITelemetryAppender { } } -class TestMeteredConnectionService implements IMeteredConnectionService { - declare readonly _serviceBrand: undefined; - readonly onDidChangeIsConnectionMetered = Event.None; - - constructor( - readonly isConnectionMetered: boolean, - readonly whenInitialized: Promise, - ) { } -} - class ErrorTestingSettings { public personalInfo: string; public importantInfo: string; @@ -149,70 +137,6 @@ suite('TelemetryService', () => { service.dispose(); })); - test('buffers events until the metered connection state is initialized', async () => { - const initialized = new DeferredPromise(); - const testAppender = new TestTelemetryAppender(); - const service = new TelemetryService({ - appenders: [testAppender], - meteredConnectionService: new TestMeteredConnectionService(false, initialized.p), - }, new TestConfigurationService(), TestProductService); - - service.publicLog('testEvent'); - assert.strictEqual(testAppender.getEventsCount(), 0); - - initialized.complete(); - await initialized.p; - await Promise.resolve(); - - assert.strictEqual(testAppender.getEventsCount(), 1); - service.dispose(); - }); - - test('drops buffered events when the initialized connection is metered', async () => { - const initialized = new DeferredPromise(); - const testAppender = new TestTelemetryAppender(); - const service = new TelemetryService({ - appenders: [testAppender], - meteredConnectionService: new TestMeteredConnectionService(true, initialized.p), - }, new TestConfigurationService(), TestProductService); - - service.publicLog('testEvent'); - initialized.complete(); - await initialized.p; - await Promise.resolve(); - - assert.strictEqual(testAppender.getEventsCount(), 0); - service.dispose(); - }); - - test('flushes buffered events on dispose while the metered connection state is pending', () => { - const initialized = new DeferredPromise(); - const testAppender = new TestTelemetryAppender(); - const service = new TelemetryService({ - appenders: [testAppender], - meteredConnectionService: new TestMeteredConnectionService(false, initialized.p), - }, new TestConfigurationService(), TestProductService); - - service.publicLog('testEvent'); - service.dispose(); - - assert.strictEqual(testAppender.getEventsCount(), 1); - }); - - test('drops buffered events on dispose while the pending connection state is conservatively metered', () => { - const initialized = new DeferredPromise(); - const testAppender = new TestTelemetryAppender(); - const service = new TelemetryService({ - appenders: [testAppender], - meteredConnectionService: new TestMeteredConnectionService(true, initialized.p), - }, new TestConfigurationService(), TestProductService); - - service.publicLog('testEvent'); - service.dispose(); - - assert.strictEqual(testAppender.getEventsCount(), 0); - }); - test('Fixed telemetry level does not require a configuration service', sinonTestFn(function () { const testAppender = new TestTelemetryAppender(); const service = TelemetryService.createWithLevel({ diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 8fb9b2e40f48c0..4d60c3b4305fdd 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -105,11 +105,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } else { this.logService.info('[AutoSync] Disabled.'); } - if (this.meteredConnectionService.isConnectionMetered) { - void this.initializeAutoSync(); - } else { - this.updateAutoSync(); - } + this.updateAutoSync(); if (this.hasToDisableMachineEventually()) { this.disableMachineEventually(); @@ -124,13 +120,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } } - private async initializeAutoSync(): Promise { - await this.meteredConnectionService.whenInitialized; - if (!this._store.isDisposed) { - this.updateAutoSync(); - } - } - private updateAutoSync(): void { const { enabled, message } = this.isAutoSyncEnabled(); if (enabled) { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index ba543566b148bd..058eab04cfb526 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -102,7 +102,7 @@ export class UserDataSyncClient extends Disposable { await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); - this.instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: new Emitter().event }); + this.instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: new Emitter().event }); this.instantiationService.stub(IRequestService, this.testServer); diff --git a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts index a1c6e97a4244c4..712cb3c1d84954 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts @@ -64,7 +64,6 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi } private async _triggerAutoUpdate(marketplaceIds: ReadonlySet): Promise { - await this._meteredConnectionService.whenInitialized; if (this._store.isDisposed || this._updateInFlight || this._meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index cfd0b8d1515934..ac3f0376b56078 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -410,7 +410,7 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke this._register(runWhenGlobalIdle(() => { this._updateChecksInitialized = true; - void this._initializeUpdateChecks(); + this._scheduleUpdateCheck(); this._register(Event.filter( _configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(AutoUpdateConfigurationKey) @@ -442,13 +442,6 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke })); } - private async _initializeUpdateChecks(): Promise { - await this._meteredConnectionService.whenInitialized; - if (!this._store.isDisposed) { - this._scheduleUpdateCheck(); - } - } - clearUpdatesAvailable(marketplaceIds?: ReadonlySet): void { const remaining = marketplaceIds ? new Set([...this._marketplacesWithUpdates.get()].filter(id => !marketplaceIds.has(id))) @@ -885,11 +878,6 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke } private async _doRunUpdateCheck(): Promise { - await this._meteredConnectionService.whenInitialized; - if (this._store.isDisposed) { - return; - } - if (this._meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts index bda92afd3df700..663ae6ba2af01e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { DeferredPromise } from '../../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; @@ -46,13 +45,9 @@ suite('PluginAutoUpdate', () => { clearUpdatesAvailableCalls: ReadonlySet[]; } - function createContribution( - stateOverrides?: Partial, - isConnectionMetered = false, - whenInitialized: Promise = Promise.resolve(), - ): { contribution: PluginAutoUpdate; state: MockState; meteredConnectionService: TestMeteredConnectionService } { + function createContribution(stateOverrides?: Partial, isConnectionMetered = false): { contribution: PluginAutoUpdate; state: MockState; meteredConnectionService: TestMeteredConnectionService } { const instantiationService = store.add(new TestInstantiationService()); - const meteredConnectionService = store.add(new TestMeteredConnectionService(isConnectionMetered, whenInitialized)); + const meteredConnectionService = store.add(new TestMeteredConnectionService(isConnectionMetered)); const state: MockState = { marketplacesWithUpdates: observableValue>('test.marketplacesWithUpdates', new Set()), @@ -109,21 +104,6 @@ suite('PluginAutoUpdate', () => { })), [{ silent: true, automatic: true, marketplaceIds: ['github:microsoft/plugins'] }]); }); - test('waits for connection state initialization before updating', async () => { - const initialized = new DeferredPromise(); - const { state } = createContribution(undefined, false, initialized.p); - - state.marketplacesWithUpdates.set(new Set(['github:microsoft/plugins']), undefined); - await flushMicrotasks(); - assert.strictEqual(state.updateAllCalls.length, 0); - - initialized.complete(); - await flushMicrotasks(); - await flushMicrotasks(); - - assert.deepStrictEqual(state.updateAllCalls.map(call => [...call.marketplaceIds ?? []]), [['github:microsoft/plugins']]); - }); - test('retains queued updates while metered and runs them when unmetered', async () => { const { state, meteredConnectionService } = createContribution(undefined, true); diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts index 432d075aa8614b..c3d08a30792784 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts @@ -788,41 +788,6 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => { assert.strictEqual(fetchCount, 1); }); - test('periodic update checking waits for metered connection initialization', async () => { - let runIdle: ((idle: IdleDeadline) => void) | undefined; - store.add(installFakeRunWhenIdle((_target, runner) => { - runIdle = runner; - return Disposable.None; - })); - const initialized = new DeferredPromise(); - const meteredConnectionService = store.add(new TestMeteredConnectionService(false, initialized.p)); - let fetchCount = 0; - const service = createService({ - meteredConnectionService, - pluginRepositoryService: { - fetchRepository: async () => { - fetchCount++; - return false; - }, - }, - }); - service.addInstalledPlugin( - URI.file('/agent-plugins/github.com/microsoft/plugins/my-plugin'), - makePlugin('my-plugin', 'my-plugin'), - ); - - assert.ok(runIdle); - runIdle({ didTimeout: false, timeRemaining: () => 50 }); - await timeout(0); - assert.strictEqual(fetchCount, 0); - - initialized.complete(); - await timeout(0); - await timeout(0); - - assert.strictEqual(fetchCount, 1); - }); - test('defers an overdue check until queued updates are acknowledged', async () => { const updateCheckInterval = 24 * 60 * 60 * 1000; const clock = sinon.useFakeTimers({ now: updateCheckInterval + 1 }); @@ -1028,7 +993,6 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => { assert.ok(runIdle); runIdle({ didTimeout: false, timeRemaining: () => 50 }); await timeout(0); - await timeout(0); assert.deepStrictEqual(fetched, [deferredRef.canonicalId]); await configurationService.setUserConfiguration(ChatConfiguration.StrictMarketplaces, [ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 0a964843e10fcc..b60ffca7db1ed4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1155,12 +1155,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this._onChange.fire(undefined); } if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) { - void this.checkForUpdatesAutomatically(`Enabled auto check updates`); + if (this.isAutoCheckUpdatesEnabled()) { + this.checkForUpdates(`Enabled auto check updates`); + } } })); this._register(this.extensionEnablementService.onEnablementChanged(platformExtensions => { - if (this.getAutoUpdateValue() === 'on' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) { - void this.checkForUpdatesAutomatically('Extension enablement changed'); + if (this.isAutoCheckUpdatesEnabled() && this.getAutoUpdateValue() === 'on' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) { + this.checkForUpdates('Extension enablement changed'); } })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); @@ -1170,16 +1172,22 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension owner: 'sandy081'; comment: 'Report when update check is triggered on product update'; }>('extensions:updatecheckonproductupdate'); - void this.checkForUpdatesAutomatically('Product update'); + if (this.isAutoCheckUpdatesEnabled()) { + this.checkForUpdates('Product update'); + } } })); this._register(this.allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => { - void this.checkForUpdatesAutomatically('Allowed extensions changed'); + if (this.isAutoCheckUpdatesEnabled()) { + this.checkForUpdates('Allowed extensions changed'); + } })); this._register(this.meteredConnectionService.onDidChangeIsConnectionMetered(() => { - void this.checkForUpdatesAutomatically('Connection is no longer metered'); + if (this.isAutoCheckUpdatesEnabled()) { + this.checkForUpdates('Connection is no longer metered'); + } if (isWeb && !this.isAutoUpdateEnabled()) { this.autoUpdateBuiltinExtensions(); } @@ -2217,26 +2225,13 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.configurationService.getValue(AutoCheckUpdatesConfigurationKey); } - private async checkForUpdatesAutomatically(reason?: string): Promise { - await this.meteredConnectionService.whenInitialized; - if (!this._store.isDisposed && this.isAutoCheckUpdatesEnabled()) { - await this.checkForUpdates(reason); - } - } - private eventuallyCheckForUpdates(immediate = false): void { this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { - await this.meteredConnectionService.whenInitialized; - if (this._store.isDisposed) { - return; - } if (this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } - if (!this._store.isDisposed) { - this.eventuallyCheckForUpdates(); - } + this.eventuallyCheckForUpdates(); }, immediate ? 0 : this.getUpdatesCheckInterval()).then(undefined, err => null); } @@ -2253,8 +2248,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateBuiltinExtensions(): Promise { - await this.meteredConnectionService.whenInitialized; - if (this._store.isDisposed || this.meteredConnectionService.isConnectionMetered) { + if (this.meteredConnectionService.isConnectionMetered) { return; } await this.checkForUpdates(undefined, true); @@ -2278,10 +2272,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateExtensions(): Promise { - await this.meteredConnectionService.whenInitialized; - if (this._store.isDisposed) { - return; - } if (this.meteredConnectionService.isConnectionMetered) { this.logService.trace('[Extensions]: Skipping auto-update because connection is metered'); return; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index 81fe63ca88d096..d47074727c43c7 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -296,7 +296,7 @@ suite('ExtensionRecommendationsService Test', () => { }); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 20e6edb6cc6ff3..f27872132feffd 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -160,7 +160,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index ec56ddbf10d370..2d7bfc8d6780a5 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -224,7 +224,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); queryPage = aPage([]); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index d6b745ea114782..65a7e0f251a156 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -5,7 +5,6 @@ import * as sinon from 'sinon'; import assert from 'assert'; -import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey, AutoUpdateDelayConfigurationKey, ExtensionRuntimeActionType, AutoUpdateConfigurationValue } from '../../common/extensions.js'; import { ExtensionsWorkbenchService } from '../../browser/extensionsWorkbenchService.js'; @@ -158,7 +157,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); - instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, whenInitialized: Promise.resolve(), onDidChangeIsConnectionMetered: Event.None }); + instantiationService.stub(IMeteredConnectionService, { isConnectionMetered: false, onDidChangeIsConnectionMetered: Event.None }); }); test('test gallery extension', async () => { @@ -1779,31 +1778,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); - test('waits for metered connection initialization before checking for updates automatically', async () => { - const initialized = new DeferredPromise(); - instantiationService.stub(IMeteredConnectionService, { - isConnectionMetered: false, - whenInitialized: initialized.p, - onDidChangeIsConnectionMetered: Event.None, - }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a')]); - let getExtensionsCount = 0; - instantiationService.stub(IExtensionGalleryService, 'getExtensions', async () => { - getExtensionsCount++; - return []; - }); - - testObject = await aWorkbenchService(); - await timeout(0); - assert.strictEqual(getExtensionsCount, 0); - - initialized.complete(); - await timeout(0); - await timeout(0); - - assert.strictEqual(getExtensionsCount, 1); - }); - async function aWorkbenchService(): Promise { const workbenchService: ExtensionsWorkbenchService = disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService)); await workbenchService.queryLocal(); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index daa2c00fdd13e5..eb64abc71422cb 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -17,7 +17,6 @@ import { ITelemetryServiceConfig, TelemetryService as BaseTelemetryService } fro import { getTelemetryLevel, isInternalTelemetry, isLoggingOnly, ITelemetryAppender, NullTelemetryService, supportsTelemetry } from '../../../../platform/telemetry/common/telemetryUtils.js'; import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; -import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { resolveWorkbenchCommonProperties } from './workbenchCommonProperties.js'; import { experimentsEnabled } from '../common/workbenchTelemetryUtils.js'; @@ -44,17 +43,16 @@ export class TelemetryService extends Disposable implements ITelemetryService { @IStorageService storageService: IStorageService, @IProductService productService: IProductService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IMeteredConnectionService meteredConnectionService: IMeteredConnectionService, @IRequestService requestService: IRequestService ) { super(); - this.impl = this.initializeService(environmentService, loggerService, configurationService, storageService, productService, remoteAgentService, meteredConnectionService); + this.impl = this.initializeService(environmentService, loggerService, configurationService, storageService, productService, remoteAgentService); // When the level changes it could change from off to on and we want to make sure telemetry is properly intialized this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TELEMETRY_SETTING_ID)) { - this.impl = this.initializeService(environmentService, loggerService, configurationService, storageService, productService, remoteAgentService, meteredConnectionService); + this.impl = this.initializeService(environmentService, loggerService, configurationService, storageService, productService, remoteAgentService); } })); @@ -93,8 +91,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { configurationService: IConfigurationService, storageService: IStorageService, productService: IProductService, - remoteAgentService: IRemoteAgentService, - meteredConnectionService: IMeteredConnectionService + remoteAgentService: IRemoteAgentService ) { const telemetrySupported = supportsTelemetry(productService, environmentService) && productService.aiConfig?.ariaKey; if (telemetrySupported && getTelemetryLevel(configurationService) !== TelemetryLevel.NONE && this.impl === NullTelemetryService) { @@ -123,7 +120,6 @@ export class TelemetryService extends Disposable implements ITelemetryService { piiPaths: [mainWindow.location.origin], sendErrorTelemetry: this.sendErrorTelemetry, waitForExperimentProperties: experimentsEnabled(configurationService, productService, environmentService), - meteredConnectionService, }; return this._register(new BaseTelemetryService(config, configurationService, productService)); From 58388b0aeac5908f4e68da38951d1591390ffb22 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Sun, 20 Sep 2026 09:29:40 -0700 Subject: [PATCH 11/11] metered: restore auto default and honor explicit update preferences Remove the Insiders-only default condition so all build qualities retain auto detection. Compare the configured extension auto-update preference, not network-gated availability, before an explicit preference change. Cover pending native initialization, metered enablement, cancellation and redundant actions while preserving conservative gating for automatic requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../meteredConnection.config.contribution.ts | 3 +- .../browser/extensionsWorkbenchService.ts | 2 +- .../extensionsWorkbenchService.test.ts | 76 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts b/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts index 67e7aff827355f..1259f403a12ecf 100644 --- a/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts +++ b/src/vs/platform/meteredConnection/common/meteredConnection.config.contribution.ts @@ -5,7 +5,6 @@ import { localize } from '../../../nls.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; -import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -23,7 +22,7 @@ configurationRegistry.registerConfiguration({ localize('meteredConnection.on', "Always treat the network connection as metered. Automatic updates and downloads will be postponed."), localize('meteredConnection.off', "Never treat the network connection as metered.") ], - default: product.quality === 'insider' ? 'auto' : 'off', + default: 'auto', scope: ConfigurationScope.APPLICATION, description: localize('meteredConnection', "Controls whether the current network connection should be treated as metered. When metered, automatic updates, extension downloads, and other background network activity will be postponed to reduce data usage."), tags: ['usesOnlineServices'] diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index b60ffca7db1ed4..d56576c7c73cdd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1280,7 +1280,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } async updateAutoUpdateForAllExtensions(isAutoUpdateEnabled: boolean): Promise { - const wasAutoUpdateEnabled = this.isAutoUpdateEnabled(); + const wasAutoUpdateEnabled = this.getAutoUpdateValue() !== 'off'; if (wasAutoUpdateEnabled === isAutoUpdateEnabled) { return; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 65a7e0f251a156..3b28f94908aa86 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -5,6 +5,7 @@ import * as sinon from 'sinon'; import assert from 'assert'; +import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey, AutoUpdateDelayConfigurationKey, ExtensionRuntimeActionType, AutoUpdateConfigurationValue } from '../../common/extensions.js'; import { ExtensionsWorkbenchService } from '../../browser/extensionsWorkbenchService.js'; @@ -1727,6 +1728,81 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); + test('Test disable autoupdate while metered initialization is pending', async () => { + stubConfiguration('on'); + const initialized = new DeferredPromise(); + const meteredConnectionService = { + isConnectionMetered: true, + whenInitialized: initialized.p, + onDidChangeIsConnectionMetered: Event.None, + }; + instantiationService.stub(IMeteredConnectionService, meteredConnectionService); + let confirmationCount = 0; + instantiationService.stub(IDialogService, { + confirm: async () => { + confirmationCount++; + return { confirmed: true }; + }, + }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a')]); + let galleryRequests = 0; + instantiationService.stub(IExtensionGalleryService, 'getExtensions', async () => { + galleryRequests++; + return []; + }); + testObject = await aWorkbenchService(); + await timeout(0); + + await testObject.updateAutoUpdateForAllExtensions(false); + const configurationService = instantiationService.get(IConfigurationService); + const beforeInitialization = { + confirmationCount, + autoUpdate: configurationService.getValue(AutoUpdateConfigurationKey), + galleryRequests, + }; + meteredConnectionService.isConnectionMetered = false; + await initialized.complete(); + + assert.deepStrictEqual({ + beforeInitialization, + autoUpdateAfterInitialization: configurationService.getValue(AutoUpdateConfigurationKey), + }, { + beforeInitialization: { confirmationCount: 1, autoUpdate: 'off', galleryRequests: 0 }, + autoUpdateAfterInitialization: 'off', + }); + }); + + for (const { name, currentValue, requestedValue, confirmed, expectedConfirmationCount, expectedValue } of [ + { name: 'enable autoupdate', currentValue: 'off', requestedValue: true, confirmed: true, expectedConfirmationCount: 1, expectedValue: 'on' }, + { name: 'cancel disabling autoupdate', currentValue: 'on', requestedValue: false, confirmed: false, expectedConfirmationCount: 1, expectedValue: 'on' }, + { name: 'leave enabled autoupdate unchanged', currentValue: 'on', requestedValue: true, confirmed: true, expectedConfirmationCount: 0, expectedValue: 'on' }, + { name: 'leave disabled autoupdate unchanged', currentValue: 'off', requestedValue: false, confirmed: true, expectedConfirmationCount: 0, expectedValue: 'off' }, + ]) { + test(`Test ${name} while metered`, async () => { + stubConfiguration(currentValue); + instantiationService.stub(IMeteredConnectionService, { + isConnectionMetered: true, + whenInitialized: Promise.resolve(), + onDidChangeIsConnectionMetered: Event.None, + }); + let confirmationCount = 0; + instantiationService.stub(IDialogService, { + confirm: async () => { + confirmationCount++; + return { confirmed }; + }, + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateForAllExtensions(requestedValue); + + assert.deepStrictEqual({ + confirmationCount, + autoUpdate: instantiationService.get(IConfigurationService).getValue(AutoUpdateConfigurationKey), + }, { confirmationCount: expectedConfirmationCount, autoUpdate: expectedValue }); + }); + } + test('Test reset autoupdate extensions state when auto update is disabled', async () => { instantiationService.stub(IDialogService, { confirm: () => Promise.resolve({ confirmed: true })