mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-07 15:26:49 +01:00
extensions: add delayed auto-update mode to extensions.autoUpdate (#318974)
* extensions: add delayed auto-update mode to extensions.autoUpdate
Folds a 6-hour delayed auto-update mode into the existing
`extensions.autoUpdate` setting, which is now a string enum
('on' | 'delayed' | 'off') and policy-controllable via the
ExtensionsAutoUpdate policy.
When set to 'delayed', an extension whose newer version was published
less than 6 hours ago is treated as outdated-but-delayed: it is excluded
from auto-update and the activity badge, and surfaces an info status
explaining when it will be auto updated. Manual updates still work.
Includes startup migration from legacy values (true/'all'/
'onlyEnabledExtensions' -> 'on'; false/'none'/'onlySelectedExtensions'
-> 'off') plus defensive read-time normalization, and unit tests for the
new logic.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* update policy file
* extensions: address code review feedback for delayed auto-update
- Type IExtensionsConfiguration.autoUpdate as AutoUpdateConfigurationValue
- Keep ExtensionStatusAction.update() returning void; add recomputeStatus()
for callers that need to await the computed status
- Preserve the higher-priority warning CSS class when the delayed info status
is also applied to an outdated extension
- Hover awaits recomputeStatus() instead of update()
- Update pluginAutoUpdate doc comment to the on/delayed/off enum values
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* update policies data
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
52cc23534f
commit
da03e4ef04
@@ -312,6 +312,40 @@
|
||||
"default": true,
|
||||
"included": true
|
||||
},
|
||||
{
|
||||
"key": "extensions.autoUpdate",
|
||||
"name": "ExtensionsAutoUpdate",
|
||||
"category": "Extensions",
|
||||
"minimumVersion": "1.122",
|
||||
"localization": {
|
||||
"description": {
|
||||
"key": "extensions.autoUpdate",
|
||||
"value": "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."
|
||||
},
|
||||
"enumDescriptions": [
|
||||
{
|
||||
"key": "extensions.autoUpdate.on",
|
||||
"value": "Enabled extensions are updated automatically."
|
||||
},
|
||||
{
|
||||
"key": "extensions.autoUpdate.delayed",
|
||||
"value": "Enabled extensions are updated automatically, but only 6 hours after a new version has been published."
|
||||
},
|
||||
{
|
||||
"key": "extensions.autoUpdate.off",
|
||||
"value": "Extensions are not updated automatically."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "string",
|
||||
"default": "on",
|
||||
"enum": [
|
||||
"on",
|
||||
"delayed",
|
||||
"off"
|
||||
],
|
||||
"included": true
|
||||
},
|
||||
{
|
||||
"key": "chat.tools.terminal.enableAutoApprove",
|
||||
"name": "ChatToolsTerminalEnableAutoApprove",
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IWorkbenchContribution } from '../../../common/contributions.js';
|
||||
import { AutoUpdateConfigurationKey, AutoUpdateConfigurationValue } from '../../extensions/common/extensions.js';
|
||||
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
|
||||
import { IPluginInstallService } from '../common/plugins/pluginInstallService.js';
|
||||
import { IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js';
|
||||
|
||||
@@ -23,11 +22,10 @@ import { IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceSe
|
||||
* Without this contribution, that signal was never consumed and plugins
|
||||
* were never auto-updated (see microsoft/vscode#308563).
|
||||
*
|
||||
* When the signal becomes `true` and `extensions.autoUpdate === true`, we
|
||||
* silently update all installed plugins. Other auto-update modes
|
||||
* (`'onlyEnabledExtensions'`, `'onlySelectedExtensions'`) gate updates on a
|
||||
* per-extension opt-in that has no plugin equivalent, so they are treated
|
||||
* the same as `false` for plugins.
|
||||
* When the signal becomes `true` and `extensions.autoUpdate` is set to update
|
||||
* extensions (`'on'` or `'delayed'`), we silently update all installed plugins.
|
||||
* The `'off'` mode gates updates on a per-extension opt-in that has no plugin
|
||||
* equivalent, so it is treated the same as disabled for plugins.
|
||||
*
|
||||
* The flag is cleared after every attempt — including failures — so the
|
||||
* next periodic check's `false → true` transition can always re-trigger the
|
||||
@@ -43,7 +41,7 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi
|
||||
constructor(
|
||||
@IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService,
|
||||
@IPluginInstallService private readonly _pluginInstallService: IPluginInstallService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
@@ -61,8 +59,8 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi
|
||||
return;
|
||||
}
|
||||
|
||||
const autoUpdate = this._configurationService.getValue<AutoUpdateConfigurationValue>(AutoUpdateConfigurationKey);
|
||||
if (autoUpdate !== true) {
|
||||
const autoUpdate = this._extensionsWorkbenchService.getAutoUpdateValue();
|
||||
if (autoUpdate === 'off') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ObservableMemento, observableMemento } from '../../../../../platform/ob
|
||||
import { asJson, IRequestService } from '../../../../../platform/request/common/request.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
|
||||
import type { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';
|
||||
import { AutoUpdateConfigurationKey, AutoUpdateConfigurationValue } from '../../../extensions/common/extensions.js';
|
||||
import { AutoUpdateConfigurationKey, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
|
||||
import { ChatConfiguration } from '../constants.js';
|
||||
import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js';
|
||||
import { FileBackedInstalledPluginsStore, IStoredInstalledPlugin } from './fileBackedInstalledPluginsStore.js';
|
||||
@@ -304,6 +304,7 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IWorkspacePluginSettingsService private readonly _workspacePluginSettingsService: IWorkspacePluginSettingsService,
|
||||
@IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService,
|
||||
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -723,8 +724,8 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke
|
||||
|
||||
// --- Periodic update check ------------------------------------------------
|
||||
|
||||
private _isAutoUpdateEnabled(): AutoUpdateConfigurationValue {
|
||||
return this._configurationService.getValue<AutoUpdateConfigurationValue>(AutoUpdateConfigurationKey);
|
||||
private _isAutoUpdateEnabled(): boolean {
|
||||
return this._extensionsWorkbenchService.getAutoUpdateValue() !== 'off';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,11 +7,9 @@ import assert from 'assert';
|
||||
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
|
||||
import { observableValue } from '../../../../../../base/common/observable.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js';
|
||||
import { AutoUpdateConfigurationKey } from '../../../../extensions/common/extensions.js';
|
||||
import { AutoUpdateConfigurationValue, IExtensionsWorkbenchService } from '../../../../extensions/common/extensions.js';
|
||||
import { PluginAutoUpdate } from '../../../browser/pluginAutoUpdate.js';
|
||||
import { IPluginInstallService, IUpdateAllPluginsOptions, IUpdateAllPluginsResult } from '../../../common/plugins/pluginInstallService.js';
|
||||
import { IPluginMarketplaceService } from '../../../common/plugins/pluginMarketplaceService.js';
|
||||
@@ -26,7 +24,7 @@ suite('PluginAutoUpdate', () => {
|
||||
clearUpdatesAvailableCalls: number;
|
||||
}
|
||||
|
||||
function createContribution(autoUpdate: unknown, stateOverrides?: Partial<MockState>): { contribution: PluginAutoUpdate; state: MockState } {
|
||||
function createContribution(autoUpdate: AutoUpdateConfigurationValue, stateOverrides?: Partial<MockState>): { contribution: PluginAutoUpdate; state: MockState } {
|
||||
const instantiationService = store.add(new TestInstantiationService());
|
||||
|
||||
const state: MockState = {
|
||||
@@ -52,9 +50,9 @@ suite('PluginAutoUpdate', () => {
|
||||
},
|
||||
} as Partial<IPluginInstallService> as IPluginInstallService);
|
||||
|
||||
const configService = new TestConfigurationService();
|
||||
configService.setUserConfiguration(AutoUpdateConfigurationKey, autoUpdate);
|
||||
instantiationService.stub(IConfigurationService, configService);
|
||||
instantiationService.stub(IExtensionsWorkbenchService, {
|
||||
getAutoUpdateValue: () => autoUpdate,
|
||||
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
|
||||
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
|
||||
@@ -67,14 +65,14 @@ suite('PluginAutoUpdate', () => {
|
||||
return new Promise(resolve => queueMicrotask(resolve));
|
||||
}
|
||||
|
||||
test('does not trigger update on construction when flag is false', async () => {
|
||||
const { state } = createContribution(true);
|
||||
test('does not trigger update on construction', async () => {
|
||||
const { state } = createContribution('on');
|
||||
await flushMicrotasks();
|
||||
assert.deepStrictEqual(state.updateAllCalls, []);
|
||||
});
|
||||
|
||||
test('triggers silent updateAllPlugins when hasUpdatesAvailable becomes true', async () => {
|
||||
const { state } = createContribution(true);
|
||||
const { state } = createContribution('on');
|
||||
|
||||
state.hasUpdatesAvailable.set(true, undefined);
|
||||
await flushMicrotasks();
|
||||
@@ -82,8 +80,8 @@ suite('PluginAutoUpdate', () => {
|
||||
assert.deepStrictEqual(state.updateAllCalls, [{ silent: true }]);
|
||||
});
|
||||
|
||||
test('does not trigger update when extensions.autoUpdate is false', async () => {
|
||||
const { state } = createContribution(false);
|
||||
test('does not trigger update when extensions.autoUpdate is off', async () => {
|
||||
const { state } = createContribution('off');
|
||||
|
||||
state.hasUpdatesAvailable.set(true, undefined);
|
||||
await flushMicrotasks();
|
||||
@@ -91,17 +89,13 @@ suite('PluginAutoUpdate', () => {
|
||||
assert.deepStrictEqual(state.updateAllCalls, []);
|
||||
});
|
||||
|
||||
test('does not trigger update for non-true auto-update values like onlyEnabledExtensions', async () => {
|
||||
// Plugins have no per-item opt-in equivalent to extensions, so only
|
||||
// `true` (update everything) opts plugins into auto-update.
|
||||
for (const value of ['onlyEnabledExtensions', 'onlySelectedExtensions']) {
|
||||
const { state } = createContribution(value);
|
||||
test('triggers update for the delayed auto-update mode', async () => {
|
||||
const { state } = createContribution('delayed');
|
||||
|
||||
state.hasUpdatesAvailable.set(true, undefined);
|
||||
await flushMicrotasks();
|
||||
state.hasUpdatesAvailable.set(true, undefined);
|
||||
await flushMicrotasks();
|
||||
|
||||
assert.deepStrictEqual(state.updateAllCalls, [], `expected no update for autoUpdate=${value}`);
|
||||
}
|
||||
assert.deepStrictEqual(state.updateAllCalls, [{ silent: true }]);
|
||||
});
|
||||
|
||||
test('does not run a second update concurrently with one in flight', async () => {
|
||||
@@ -109,7 +103,7 @@ suite('PluginAutoUpdate', () => {
|
||||
const pendingUpdate = new Promise<IUpdateAllPluginsResult>(resolve => {
|
||||
resolveUpdate = () => resolve({ updatedNames: [], failedNames: [] });
|
||||
});
|
||||
const { state } = createContribution(true, {
|
||||
const { state } = createContribution('on', {
|
||||
updateAllImpl: () => pendingUpdate,
|
||||
});
|
||||
|
||||
@@ -129,7 +123,7 @@ suite('PluginAutoUpdate', () => {
|
||||
});
|
||||
|
||||
test('continues running on subsequent cycles after the previous update finished', async () => {
|
||||
const { state } = createContribution(true);
|
||||
const { state } = createContribution('on');
|
||||
|
||||
state.hasUpdatesAvailable.set(true, undefined);
|
||||
await flushMicrotasks();
|
||||
@@ -146,7 +140,7 @@ suite('PluginAutoUpdate', () => {
|
||||
});
|
||||
|
||||
test('swallows errors from updateAllPlugins', async () => {
|
||||
const { state } = createContribution(true, {
|
||||
const { state } = createContribution('on', {
|
||||
updateAllImpl: async () => { throw new Error('boom'); },
|
||||
});
|
||||
|
||||
@@ -169,7 +163,7 @@ suite('PluginAutoUpdate', () => {
|
||||
// path in `PluginInstallService.updateAllPlugins`). Without our own
|
||||
// clear in `finally`, the observable would stay stuck at `true` and
|
||||
// the next periodic check's `set(true)` would not notify subscribers.
|
||||
const { state } = createContribution(true, {
|
||||
const { state } = createContribution('on', {
|
||||
updateAllImpl: async () => ({ updatedNames: [], failedNames: ['plugin-a'] }),
|
||||
});
|
||||
|
||||
@@ -190,7 +184,7 @@ suite('PluginAutoUpdate', () => {
|
||||
});
|
||||
|
||||
test('clears the flag even when updateAllPlugins throws', async () => {
|
||||
const { state } = createContribution(true, {
|
||||
const { state } = createContribution('on', {
|
||||
updateAllImpl: async () => { throw new Error('boom'); },
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { IRequestService } from '../../../../../../platform/request/common/reque
|
||||
import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
|
||||
import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js';
|
||||
import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js';
|
||||
import { IExtensionsWorkbenchService } from '../../../../extensions/common/extensions.js';
|
||||
import { ChatConfiguration } from '../../../common/constants.js';
|
||||
import { IAgentPluginRepositoryService } from '../../../common/plugins/agentPluginRepositoryService.js';
|
||||
import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceReferenceKind, MarketplaceType, PluginMarketplaceService, PluginSourceKind, getPluginSourceLabel, parseMarketplaceReference, parseMarketplaceReferences, parsePluginSource } from '../../../common/plugins/pluginMarketplaceService.js';
|
||||
@@ -257,6 +258,9 @@ suite('PluginMarketplaceService - GitHub marketplace refs', () => {
|
||||
isWorkspaceTrusted: () => true,
|
||||
onDidChangeTrust: Event.None,
|
||||
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
|
||||
instantiationService.stub(IExtensionsWorkbenchService, {
|
||||
getAutoUpdateValue: () => 'on',
|
||||
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
|
||||
|
||||
const service = store.add(instantiationService.createInstance(PluginMarketplaceService));
|
||||
await service.fetchMarketplacePlugins(CancellationToken.None);
|
||||
@@ -293,6 +297,9 @@ suite('PluginMarketplaceService - getMarketplacePluginMetadata', () => {
|
||||
isWorkspaceTrusted: () => true,
|
||||
onDidChangeTrust: Event.None,
|
||||
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
|
||||
instantiationService.stub(IExtensionsWorkbenchService, {
|
||||
getAutoUpdateValue: () => 'on',
|
||||
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
|
||||
|
||||
return store.add(instantiationService.createInstance(PluginMarketplaceService));
|
||||
}
|
||||
@@ -369,6 +376,9 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => {
|
||||
isWorkspaceTrusted: () => true,
|
||||
onDidChangeTrust: Event.None,
|
||||
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
|
||||
instantiationService.stub(IExtensionsWorkbenchService, {
|
||||
getAutoUpdateValue: () => 'on',
|
||||
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
|
||||
|
||||
return store.add(instantiationService.createInstance(PluginMarketplaceService));
|
||||
}
|
||||
@@ -586,6 +596,9 @@ suite('PluginMarketplaceService - hydration after restart', () => {
|
||||
isWorkspaceTrusted: () => true,
|
||||
onDidChangeTrust: Event.None,
|
||||
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
|
||||
instantiationService.stub(IExtensionsWorkbenchService, {
|
||||
getAutoUpdateValue: () => 'on',
|
||||
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
|
||||
|
||||
const service = store.add(instantiationService.createInstance(PluginMarketplaceService));
|
||||
|
||||
@@ -636,6 +649,9 @@ suite('PluginMarketplaceService - hydration after restart', () => {
|
||||
isWorkspaceTrusted: () => true,
|
||||
onDidChangeTrust: Event.None,
|
||||
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
|
||||
instantiationService.stub(IExtensionsWorkbenchService, {
|
||||
getAutoUpdateValue: () => 'on',
|
||||
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
|
||||
return store.add(instantiationService.createInstance(PluginMarketplaceService));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ import { Action2, IAction2Options, IMenuItem, MenuId, MenuRegistry, registerActi
|
||||
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
|
||||
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
import { ExtensionGalleryManifestStatus, ExtensionGalleryResourceType, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
|
||||
import { EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, ExtensionRequestsTimeoutConfigKey, ExtensionsLocalizedLabel, FilterType, IExtensionGalleryService, IExtensionManagementService, PreferencesLocalizedLabel, SortBy, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js';
|
||||
@@ -137,21 +138,38 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
||||
type: 'object',
|
||||
properties: {
|
||||
'extensions.autoUpdate': {
|
||||
enum: [true, 'onlyEnabledExtensions', false,],
|
||||
type: 'string',
|
||||
enum: ['on', 'delayed', 'off'],
|
||||
enumItemLabels: [
|
||||
localize('all', "All Extensions"),
|
||||
localize('enabled', "Only Enabled Extensions"),
|
||||
localize('none', "None"),
|
||||
localize('on', "On"),
|
||||
localize('delayed', "Delayed"),
|
||||
localize('off', "Off"),
|
||||
],
|
||||
enumDescriptions: [
|
||||
localize('extensions.autoUpdate.true', 'Download and install updates automatically for all extensions.'),
|
||||
localize('extensions.autoUpdate.enabled', 'Download and install updates automatically only for enabled extensions.'),
|
||||
localize('extensions.autoUpdate.false', 'Extensions are not automatically updated.'),
|
||||
localize('extensions.autoUpdate.on', 'Enabled extensions are updated automatically.'),
|
||||
localize('extensions.autoUpdate.delayed', 'Enabled extensions are updated automatically, but only 6 hours after a new version has been published.'),
|
||||
localize('extensions.autoUpdate.off', 'Extensions are not updated automatically.'),
|
||||
],
|
||||
description: localize('extensions.autoUpdate', "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."),
|
||||
default: true,
|
||||
default: 'on',
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
tags: ['usesOnlineServices']
|
||||
tags: ['usesOnlineServices'],
|
||||
policy: {
|
||||
name: 'ExtensionsAutoUpdate',
|
||||
category: PolicyCategory.Extensions,
|
||||
minimumVersion: '1.122',
|
||||
localization: {
|
||||
description: {
|
||||
key: 'extensions.autoUpdate',
|
||||
value: localize('extensions.autoUpdate', "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."),
|
||||
},
|
||||
enumDescriptions: [
|
||||
{ key: 'extensions.autoUpdate.on', value: localize('extensions.autoUpdate.on', 'Enabled extensions are updated automatically.') },
|
||||
{ key: 'extensions.autoUpdate.delayed', value: localize('extensions.autoUpdate.delayed', 'Enabled extensions are updated automatically, but only 6 hours after a new version has been published.') },
|
||||
{ key: 'extensions.autoUpdate.off', value: localize('extensions.autoUpdate.off', 'Extensions are not updated automatically.') },
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
'extensions.autoCheckUpdates': {
|
||||
type: 'boolean',
|
||||
@@ -547,6 +565,7 @@ const CONTEXT_GALLERY_FILTER_CAPABILITIES = new RawContextKey<string>('galleryFi
|
||||
const CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED = new RawContextKey<boolean>('galleryAllPublicRepositorySigned', false);
|
||||
const CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED = new RawContextKey<boolean>('galleryAllPrivateRepositorySigned', false);
|
||||
const CONTEXT_GALLERY_HAS_EXTENSION_LINK = new RawContextKey<boolean>('galleryHasExtensionLink', false);
|
||||
const CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY = new RawContextKey<boolean>('extensionsAutoUpdatePolicy', false);
|
||||
|
||||
async function runAction(action: IAction): Promise<void> {
|
||||
try {
|
||||
@@ -565,6 +584,8 @@ type IExtensionActionOptions = IAction2Options & {
|
||||
|
||||
class ExtensionsContributions extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
private readonly autoUpdatePolicyContext: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
|
||||
@@ -578,6 +599,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IPluginInstallService private readonly pluginInstallService: IPluginInstallService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
const hasLocalServerContext = CONTEXT_HAS_LOCAL_SERVER.bindTo(contextKeyService);
|
||||
@@ -597,6 +619,14 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
|
||||
this.updateExtensionGalleryStatusContexts();
|
||||
this._register(extensionGalleryManifestService.onDidChangeExtensionGalleryManifestStatus(() => this.updateExtensionGalleryStatusContexts()));
|
||||
|
||||
this.autoUpdatePolicyContext = CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY.bindTo(contextKeyService);
|
||||
this.updateAutoUpdatePolicyContext();
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
|
||||
this.updateAutoUpdatePolicyContext();
|
||||
}
|
||||
}));
|
||||
extensionGalleryManifestService.getExtensionGalleryManifest()
|
||||
.then(extensionGalleryManifest => {
|
||||
this.updateGalleryCapabilitiesContexts(extensionGalleryManifest);
|
||||
@@ -607,6 +637,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
this.registerQuickAccessProvider();
|
||||
}
|
||||
|
||||
private updateAutoUpdatePolicyContext(): void {
|
||||
this.autoUpdatePolicyContext.set(this.configurationService.inspect(AutoUpdateConfigurationKey).policyValue !== undefined);
|
||||
}
|
||||
|
||||
private async updateExtensionGalleryStatusContexts(): Promise<void> {
|
||||
CONTEXT_HAS_GALLERY.bindTo(this.contextKeyService).set(this.extensionGalleryManifestService.extensionGalleryManifestStatus === ExtensionGalleryManifestStatus.Available);
|
||||
CONTEXT_EXTENSIONS_GALLERY_STATUS.bindTo(this.contextKeyService).set(this.extensionGalleryManifestService.extensionGalleryManifestStatus);
|
||||
@@ -733,7 +767,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
}
|
||||
});
|
||||
|
||||
const enableAutoUpdateWhenCondition = ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false);
|
||||
const notPolicyControlledCondition = CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY.toNegated();
|
||||
const enableAutoUpdateWhenCondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'off'), notPolicyControlledCondition);
|
||||
this.registerExtensionAction({
|
||||
id: 'workbench.extensions.action.enableAutoUpdate',
|
||||
title: localize2('enableAutoUpdate', 'Enable Auto Update for All Extensions'),
|
||||
@@ -750,7 +785,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateForAllExtensions(true)
|
||||
});
|
||||
|
||||
const disableAutoUpdateWhenCondition = ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, false);
|
||||
const disableAutoUpdateWhenCondition = ContextKeyExpr.and(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'off'), notPolicyControlledCondition);
|
||||
this.registerExtensionAction({
|
||||
id: 'workbench.extensions.action.disableAutoUpdate',
|
||||
title: localize2('disableAutoUpdate', 'Disable Auto Update for All Extensions'),
|
||||
@@ -778,7 +813,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
when: ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.or(CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER))
|
||||
}, {
|
||||
id: MenuId.ViewContainerTitle,
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainer', VIEWLET_ID), ContextKeyExpr.or(ContextKeyExpr.has(`config.${AutoUpdateConfigurationKey}`).negate(), ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'))),
|
||||
when: ContextKeyExpr.equals('viewContainer', VIEWLET_ID),
|
||||
group: '1_updates',
|
||||
order: 2
|
||||
}, {
|
||||
@@ -1445,7 +1480,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
id: ToggleAutoUpdateForExtensionAction.ID,
|
||||
title: ToggleAutoUpdateForExtensionAction.LABEL,
|
||||
category: ExtensionsLocalizedLabel,
|
||||
precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isExtensionAllowed')),
|
||||
precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'off'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isExtensionAllowed')),
|
||||
menu: {
|
||||
id: MenuId.ExtensionContext,
|
||||
group: UPDATE_ACTIONS_GROUP,
|
||||
@@ -1472,7 +1507,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
|
||||
id: ToggleAutoUpdatesForPublisherAction.ID,
|
||||
title: { value: ToggleAutoUpdatesForPublisherAction.LABEL, original: 'Auto Update (Publisher)' },
|
||||
category: ExtensionsLocalizedLabel,
|
||||
precondition: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false),
|
||||
precondition: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'off'),
|
||||
menu: {
|
||||
id: MenuId.ExtensionContext,
|
||||
group: UPDATE_ACTIONS_GROUP,
|
||||
@@ -2101,8 +2136,11 @@ Registry.as<IConfigurationMigrationRegistry>(ConfigurationMigrationExtensions.Co
|
||||
.registerConfigurationMigrations([{
|
||||
key: AutoUpdateConfigurationKey,
|
||||
migrateFn: (value, accessor) => {
|
||||
if (value === 'onlySelectedExtensions') {
|
||||
return { value: false };
|
||||
if (value === true || value === 'all' || value === 'onlyEnabledExtensions') {
|
||||
return { value: 'on' };
|
||||
}
|
||||
if (value === false || value === 'none' || value === 'onlySelectedExtensions') {
|
||||
return { value: 'off' };
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1087,7 +1087,7 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction {
|
||||
if (extension && this.allowedExtensionsService.isAllowed(extension) !== true) {
|
||||
return;
|
||||
}
|
||||
if (this.extensionsWorkbenchService.getAutoUpdateValue() === 'onlyEnabledExtensions' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) {
|
||||
if (this.extensionsWorkbenchService.getAutoUpdateValue() !== 'off' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) {
|
||||
return;
|
||||
}
|
||||
this.enabled = true;
|
||||
@@ -2771,17 +2771,32 @@ export class ExtensionStatusAction extends ExtensionAction {
|
||||
@IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService,
|
||||
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
|
||||
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super('extensions.status', '', `${ExtensionStatusAction.CLASS} hide`, false);
|
||||
this._register(this.labelService.onDidChangeFormatters(() => this.update(), this));
|
||||
this._register(this.extensionService.onDidChangeExtensions(() => this.update()));
|
||||
this._register(this.extensionFeaturesManagementService.onDidChangeAccessData(() => this.update()));
|
||||
this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update()));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
|
||||
this.update();
|
||||
}
|
||||
}));
|
||||
this.update();
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this.updateThrottler.queue(() => this.computeAndUpdateStatus());
|
||||
this.recomputeStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the status and returns a promise that resolves when the
|
||||
* computation is done. Use this when callers need to await time-sensitive
|
||||
* status content (e.g. the delayed auto-update message) before reading it.
|
||||
*/
|
||||
recomputeStatus(): Promise<void> {
|
||||
return this.updateThrottler.queue(() => this.computeAndUpdateStatus());
|
||||
}
|
||||
|
||||
private async computeAndUpdateStatus(): Promise<void> {
|
||||
@@ -2829,8 +2844,10 @@ export class ExtensionStatusAction extends ExtensionAction {
|
||||
}
|
||||
|
||||
if (this.extension.outdated) {
|
||||
let hasConsentWarning = false;
|
||||
const message = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension);
|
||||
if (message) {
|
||||
hasConsentWarning = true;
|
||||
const markdown = new MarkdownString();
|
||||
markdown.appendMarkdown(`${message} `);
|
||||
markdown.appendMarkdown(
|
||||
@@ -2843,6 +2860,11 @@ export class ExtensionStatusAction extends ExtensionAction {
|
||||
));
|
||||
this.updateStatus({ icon: warningIcon, message: markdown }, true);
|
||||
}
|
||||
if (this.extensionsWorkbenchService.isAutoUpdateDelayed(this.extension)) {
|
||||
const updateAt = fromNow(Date.now() + this.extensionsWorkbenchService.getAutoUpdateDelayRemaining(this.extension), false, true);
|
||||
// Do not override the higher-priority warning class with the info class.
|
||||
this.updateStatus({ icon: infoIcon, message: new MarkdownString(localize('autoUpdateDelayed', "This extension is not updated yet because extension auto updates are configured to be delayed by 6 hours. It will be auto updated {0}.", updateAt)) }, !hasConsentWarning);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled) {
|
||||
|
||||
@@ -1011,7 +1011,7 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution
|
||||
|
||||
if (!badge) {
|
||||
const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined);
|
||||
const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0);
|
||||
const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) && !this.extensionsWorkbenchService.isAutoUpdateDelayed(e) ? 1 : 0), 0);
|
||||
const newBadgeNumber = outdated + actionRequired.length;
|
||||
if (newBadgeNumber > 0) {
|
||||
let msg = '';
|
||||
|
||||
@@ -825,7 +825,16 @@ export class ExtensionHoverWidget extends ExtensionWidget {
|
||||
},
|
||||
this.options.target,
|
||||
{
|
||||
markdown: () => Promise.resolve(this.getHoverMarkdown()),
|
||||
markdown: async () => {
|
||||
// Recompute the status so any time-sensitive content (e.g. the
|
||||
// delayed auto-update message) reflects the current time on each hover.
|
||||
try {
|
||||
await this.extensionStatusAction.recomputeStatus();
|
||||
} catch (error) {
|
||||
// Ignore: fall back to the last computed status.
|
||||
}
|
||||
return this.getHoverMarkdown();
|
||||
},
|
||||
markdownNotSupportedFallback: undefined
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js';
|
||||
import * as semver from '../../../../base/common/semver/semver.js';
|
||||
import { Event, Emitter } from '../../../../base/common/event.js';
|
||||
import { index } from '../../../../base/common/arrays.js';
|
||||
import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise } from '../../../../base/common/async.js';
|
||||
import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise, disposableTimeout } from '../../../../base/common/async.js';
|
||||
import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';
|
||||
import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IPager, singlePagePager } from '../../../../base/common/paging.js';
|
||||
@@ -52,7 +52,7 @@ import { FileAccess } from '../../../../base/common/network.js';
|
||||
import { IIgnoredExtensionsManagementService } from '../../../../platform/userDataSync/common/ignoredExtensions.js';
|
||||
import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js';
|
||||
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { isBoolean, isDefined, isString, isUndefined } from '../../../../base/common/types.js';
|
||||
import { isDefined, isString, isUndefined } from '../../../../base/common/types.js';
|
||||
import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js';
|
||||
import { IExtensionService, IExtensionsStatus as IExtensionRuntimeStatus, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js';
|
||||
import { isWeb, language } from '../../../../base/common/platform.js';
|
||||
@@ -80,6 +80,8 @@ interface IExtensionStateProvider<T> {
|
||||
(extension: Extension): T;
|
||||
}
|
||||
|
||||
const DELAYED_AUTO_UPDATE_PERIOD = 6 * 60 * 60 * 1000; // 6 hours in milliseconds
|
||||
|
||||
interface InstalledExtensionsEvent {
|
||||
readonly extensionIds: TelemetryTrustedValue<string>;
|
||||
readonly count: number;
|
||||
@@ -998,6 +1000,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
|
||||
private installing: IExtension[] = [];
|
||||
private tasksInProgress: CancelablePromise<any>[] = [];
|
||||
private readonly delayedAutoUpdateCheckTimer = this._register(new MutableDisposable());
|
||||
|
||||
readonly whenInitialized: Promise<void>;
|
||||
|
||||
@@ -1119,9 +1122,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
// Register listeners for auto updates
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
|
||||
if (this.getAutoUpdateValue() !== 'delayed') {
|
||||
// No longer delaying — cancel any pending delayed re-check
|
||||
this.delayedAutoUpdateCheckTimer.value = undefined;
|
||||
}
|
||||
if (this.isAutoUpdateEnabled()) {
|
||||
this.eventuallyAutoUpdateExtensions();
|
||||
}
|
||||
// The auto update value affects whether an extension is shown as delayed
|
||||
this._onChange.fire(undefined);
|
||||
}
|
||||
if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) {
|
||||
if (this.isAutoCheckUpdatesEnabled()) {
|
||||
@@ -1130,7 +1139,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
}
|
||||
}));
|
||||
this._register(this.extensionEnablementService.onEnablementChanged(platformExtensions => {
|
||||
if (this.isAutoCheckUpdatesEnabled() && this.getAutoUpdateValue() === 'onlyEnabledExtensions' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) {
|
||||
if (this.isAutoCheckUpdatesEnabled() && this.getAutoUpdateValue() !== 'off' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) {
|
||||
this.checkForUpdates('Extension enablement changed');
|
||||
}
|
||||
}));
|
||||
@@ -1188,16 +1197,45 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
if (this.meteredConnectionService.isConnectionMetered) {
|
||||
return false;
|
||||
}
|
||||
return this.getAutoUpdateValue() !== false;
|
||||
return this.getAutoUpdateValue() !== 'off';
|
||||
}
|
||||
|
||||
getAutoUpdateValue(): AutoUpdateConfigurationValue {
|
||||
const autoUpdate = this.configurationService.getValue<AutoUpdateConfigurationValue>(AutoUpdateConfigurationKey);
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
if (<any>autoUpdate === 'onlySelectedExtensions') {
|
||||
const autoUpdate = this.configurationService.getValue<AutoUpdateConfigurationValue | boolean | 'all' | 'none' | 'onlyEnabledExtensions' | 'onlySelectedExtensions'>(AutoUpdateConfigurationKey);
|
||||
// Normalize legacy values
|
||||
if (autoUpdate === true || autoUpdate === 'on' || autoUpdate === 'all' || autoUpdate === 'onlyEnabledExtensions') {
|
||||
return 'on';
|
||||
}
|
||||
if (autoUpdate === false || autoUpdate === 'off' || autoUpdate === 'none' || autoUpdate === 'onlySelectedExtensions') {
|
||||
return 'off';
|
||||
}
|
||||
if (autoUpdate === 'delayed') {
|
||||
return 'delayed';
|
||||
}
|
||||
return 'on';
|
||||
}
|
||||
|
||||
isAutoUpdateDelayed(extension: IExtension): boolean {
|
||||
if (!extension.outdated) {
|
||||
return false;
|
||||
}
|
||||
return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' ? autoUpdate : true;
|
||||
if (this.getAutoUpdateValue() !== 'delayed') {
|
||||
return false;
|
||||
}
|
||||
return this.getAutoUpdateDelayRemaining(extension) > 0;
|
||||
}
|
||||
|
||||
getAutoUpdateDelayRemaining(extension: IExtension): number {
|
||||
const lastUpdated = extension.gallery?.lastUpdated;
|
||||
if (!Number.isFinite(lastUpdated) || !lastUpdated) {
|
||||
return 0;
|
||||
}
|
||||
const elapsed = Date.now() - lastUpdated;
|
||||
if (elapsed < 0) {
|
||||
// Future timestamp (clock skew) — treat as not delayed
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, DELAYED_AUTO_UPDATE_PERIOD - elapsed);
|
||||
}
|
||||
|
||||
async updateAutoUpdateForAllExtensions(isAutoUpdateEnabled: boolean): Promise<void> {
|
||||
@@ -1220,7 +1258,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
// Reset extensions enabled for auto update first to prevent them from being updated
|
||||
this.setEnabledAutoUpdateExtensions([]);
|
||||
|
||||
await this.configurationService.updateValue(AutoUpdateConfigurationKey, isAutoUpdateEnabled);
|
||||
await this.configurationService.updateValue(AutoUpdateConfigurationKey, isAutoUpdateEnabled ? 'on' : 'off');
|
||||
|
||||
this.setDisabledAutoUpdateExtensions([]);
|
||||
await this.updateExtensionsPinnedState(!isAutoUpdateEnabled);
|
||||
@@ -2139,7 +2177,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
}
|
||||
|
||||
private getUpdatesCheckInterval(): number {
|
||||
if (this.productService.quality === 'insider' && this.getProductUpdateVersion()) {
|
||||
if (
|
||||
this.productService.quality === 'insider'
|
||||
&& this.getProductUpdateVersion()
|
||||
&& this.getAutoUpdateValue() !== 'delayed'
|
||||
) {
|
||||
return 1000 * 60 * 60 * 1; // 1 hour
|
||||
}
|
||||
return ExtensionsWorkbenchService.UpdatesCheckInterval;
|
||||
@@ -2183,11 +2225,21 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
const toUpdate: IExtension[] = [];
|
||||
const disabledAutoUpdate = [];
|
||||
const consentRequired = [];
|
||||
let soonestDelayRemaining = Number.MAX_SAFE_INTEGER;
|
||||
const isDelayed = this.getAutoUpdateValue() === 'delayed';
|
||||
for (const extension of this.outdated) {
|
||||
if (!this.shouldAutoUpdateExtension(extension)) {
|
||||
disabledAutoUpdate.push(extension.identifier.id);
|
||||
continue;
|
||||
}
|
||||
if (isDelayed && !extension.local?.forceAutoUpdate) {
|
||||
const delayRemaining = this.getAutoUpdateDelayRemaining(extension);
|
||||
if (delayRemaining > 0) {
|
||||
this.logService.trace('Auto update delayed for extension', extension.identifier.id);
|
||||
soonestDelayRemaining = Math.min(soonestDelayRemaining, delayRemaining);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (await this.shouldRequireConsentToUpdate(extension)) {
|
||||
consentRequired.push(extension.identifier.id);
|
||||
continue;
|
||||
@@ -2195,6 +2247,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
toUpdate.push(extension);
|
||||
}
|
||||
|
||||
if (soonestDelayRemaining < Number.MAX_SAFE_INTEGER) {
|
||||
this.delayedAutoUpdateCheckTimer.value = disposableTimeout(() => this.eventuallyCheckForUpdates(true), soonestDelayRemaining);
|
||||
} else {
|
||||
this.delayedAutoUpdateCheckTimer.value = undefined;
|
||||
}
|
||||
|
||||
if (disabledAutoUpdate.length) {
|
||||
this.logService.trace('Auto update disabled for extensions', disabledAutoUpdate.join(', '));
|
||||
}
|
||||
@@ -2246,7 +2304,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
|
||||
const autoUpdateValue = this.getAutoUpdateValue();
|
||||
|
||||
if (autoUpdateValue === false) {
|
||||
if (autoUpdateValue === 'off') {
|
||||
const extensionsToAutoUpdate = this.getEnabledAutoUpdateExtensions();
|
||||
const extensionId = extension.identifier.id.toLowerCase();
|
||||
if (extensionsToAutoUpdate.includes(extensionId)) {
|
||||
@@ -2267,11 +2325,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
return false;
|
||||
}
|
||||
|
||||
if (autoUpdateValue === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (autoUpdateValue === 'onlyEnabledExtensions') {
|
||||
if (autoUpdateValue === 'on' || autoUpdateValue === 'delayed') {
|
||||
return extension.enablementState !== EnablementState.DisabledGlobally && extension.enablementState !== EnablementState.DisabledWorkspace;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,8 @@ export interface IExtensionsWorkbenchService {
|
||||
open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise<void>;
|
||||
openSearch(searchValue: string, focus?: boolean): Promise<void>;
|
||||
getAutoUpdateValue(): AutoUpdateConfigurationValue;
|
||||
isAutoUpdateDelayed(extension: IExtension): boolean;
|
||||
getAutoUpdateDelayRemaining(extension: IExtension): number;
|
||||
checkForUpdates(): Promise<void>;
|
||||
getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined;
|
||||
updateAll(): Promise<InstallExtensionResult[]>;
|
||||
@@ -189,10 +191,10 @@ export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates';
|
||||
export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange';
|
||||
export const AutoRestartConfigurationKey = 'extensions.autoRestart';
|
||||
|
||||
export type AutoUpdateConfigurationValue = boolean | 'onlyEnabledExtensions' | 'onlySelectedExtensions';
|
||||
export type AutoUpdateConfigurationValue = 'on' | 'delayed' | 'off';
|
||||
|
||||
export interface IExtensionsConfiguration {
|
||||
autoUpdate: boolean;
|
||||
autoUpdate: AutoUpdateConfigurationValue;
|
||||
autoCheckUpdates: boolean;
|
||||
ignoreRecommendations: boolean;
|
||||
closeExtensionDetailsOnViewChange: boolean;
|
||||
|
||||
+84
-2
@@ -424,6 +424,76 @@ suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
assert.ok(!testObject.local[0].outdated);
|
||||
});
|
||||
|
||||
test('test isAutoUpdateDelayed returns false when auto update is not set to delayed', async () => {
|
||||
stubConfiguration('on', true);
|
||||
testObject = await anOutdatedExtensionWorkbenchService(Date.now());
|
||||
|
||||
assert.strictEqual(testObject.local[0].outdated, true);
|
||||
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), false);
|
||||
});
|
||||
|
||||
test('test isAutoUpdateDelayed returns true for recently published version when auto update is set to delayed', async () => {
|
||||
stubConfiguration('delayed', true);
|
||||
testObject = await anOutdatedExtensionWorkbenchService(Date.now() - (1000 * 60 * 60) /* 1 hour ago */);
|
||||
|
||||
assert.strictEqual(testObject.local[0].outdated, true);
|
||||
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), true);
|
||||
});
|
||||
|
||||
test('test isAutoUpdateDelayed returns false for version published more than 6 hours ago', async () => {
|
||||
stubConfiguration('delayed', true);
|
||||
testObject = await anOutdatedExtensionWorkbenchService(Date.now() - (1000 * 60 * 60 * 7) /* 7 hours ago */);
|
||||
|
||||
assert.strictEqual(testObject.local[0].outdated, true);
|
||||
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), false);
|
||||
});
|
||||
|
||||
test('test isAutoUpdateDelayed returns false for version with a future published timestamp', async () => {
|
||||
stubConfiguration('delayed', true);
|
||||
testObject = await anOutdatedExtensionWorkbenchService(Date.now() + (1000 * 60 * 60) /* 1 hour in the future */);
|
||||
|
||||
assert.strictEqual(testObject.local[0].outdated, true);
|
||||
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), false);
|
||||
});
|
||||
|
||||
test('test getAutoUpdateValue normalizes configured and legacy values', async () => {
|
||||
const result: Record<string, string> = {};
|
||||
for (const value of [true, false, 'on', 'off', 'all', 'onlyEnabledExtensions', 'onlySelectedExtensions', 'delayed', 'none', 'unknown']) {
|
||||
stubConfiguration(value);
|
||||
testObject = await aWorkbenchService();
|
||||
result[String(value)] = testObject.getAutoUpdateValue();
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
'true': 'on',
|
||||
'false': 'off',
|
||||
'on': 'on',
|
||||
'off': 'off',
|
||||
'all': 'on',
|
||||
'onlyEnabledExtensions': 'on',
|
||||
'onlySelectedExtensions': 'off',
|
||||
'delayed': 'delayed',
|
||||
'none': 'off',
|
||||
'unknown': 'on',
|
||||
});
|
||||
});
|
||||
|
||||
test('test getAutoUpdateDelayRemaining returns remaining time within the delay window', async () => {
|
||||
stubConfiguration('delayed', true);
|
||||
testObject = await anOutdatedExtensionWorkbenchService(Date.now() - (1000 * 60 * 60 * 2) /* 2 hours ago */);
|
||||
|
||||
const remaining = testObject.getAutoUpdateDelayRemaining(testObject.local[0]);
|
||||
// Published 2 hours ago, so ~4 hours of the 6 hour window remain.
|
||||
assert.ok(remaining > (1000 * 60 * 60 * 3) && remaining <= (1000 * 60 * 60 * 4), `unexpected remaining time ${remaining}`);
|
||||
});
|
||||
|
||||
test('test getAutoUpdateDelayRemaining returns 0 when the published timestamp is missing', async () => {
|
||||
stubConfiguration('delayed', true);
|
||||
testObject = await anOutdatedExtensionWorkbenchService(undefined);
|
||||
|
||||
assert.strictEqual(testObject.getAutoUpdateDelayRemaining(testObject.local[0]), 0);
|
||||
});
|
||||
|
||||
test('test canInstall returns false for extensions with out gallery', async () => {
|
||||
const local = aLocalExtension('a', { version: '1.0.1' }, { type: ExtensionType.System });
|
||||
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
|
||||
@@ -1516,7 +1586,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
});
|
||||
|
||||
test('Test disable autoupdate for extension when auto update is enabled for enabled extensions', async () => {
|
||||
stubConfiguration('onlyEnabledExtensions');
|
||||
stubConfiguration('on');
|
||||
|
||||
const extension1 = aLocalExtension('a');
|
||||
const extension2 = aLocalExtension('b');
|
||||
@@ -1695,9 +1765,21 @@ suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
return workbenchService;
|
||||
}
|
||||
|
||||
async function anOutdatedExtensionWorkbenchService(lastUpdated: number | undefined): Promise<ExtensionsWorkbenchService> {
|
||||
const local = aLocalExtension('a', { version: '1.0.1' });
|
||||
const gallery = aGalleryExtension(local.manifest.name, { identifier: local.identifier, version: '1.0.2', lastUpdated });
|
||||
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery);
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', [gallery]);
|
||||
const workbenchService = await aWorkbenchService();
|
||||
await workbenchService.checkForUpdates();
|
||||
return workbenchService;
|
||||
}
|
||||
|
||||
function stubConfiguration(autoUpdateValue?: any, autoCheckUpdatesValue?: any): void {
|
||||
const values: any = {
|
||||
[AutoUpdateConfigurationKey]: autoUpdateValue ?? true,
|
||||
[AutoUpdateConfigurationKey]: autoUpdateValue ?? 'on',
|
||||
[AutoCheckUpdatesConfigurationKey]: autoCheckUpdatesValue ?? true
|
||||
};
|
||||
const emitter = disposableStore.add(new Emitter<IConfigurationChangeEvent>());
|
||||
|
||||
Reference in New Issue
Block a user