From e7323aa8b4c2f1763fa26b183d5d7bc6be34cf91 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Sun, 20 Sep 2026 08:18:11 -0700 Subject: [PATCH] extensions: wait for native metered state before auto-updates Stack extension workbench automatic checks and updates on the native metered service PR. Wait for initial state for background checks and both extension update paths, preserving manual checks and preventing work after disposal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/extensionsWorkbenchService.ts | 42 ++++++++++++------- .../extensionRecommendationsService.test.ts | 2 +- .../extensionsActions.test.ts | 2 +- .../electron-browser/extensionsViews.test.ts | 2 +- .../extensionsWorkbenchService.test.ts | 28 ++++++++++++- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index b60ffca7db1ed4..0a964843e10fcc 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,7 +2253,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateBuiltinExtensions(): Promise { - if (this.meteredConnectionService.isConnectionMetered) { + await this.meteredConnectionService.whenInitialized; + if (this._store.isDisposed || this.meteredConnectionService.isConnectionMetered) { return; } await this.checkForUpdates(undefined, true); @@ -2272,6 +2278,10 @@ 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 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();