src/vs/platform/userDataSync/common/extensionsSync.ts

645 LOC · 273 covered · 372 uncovered · 44 ranges · 585 concepts · 3 introducers · 348 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- abstractSynchronizer.ts ×49
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Promises } from '../../../base/common/async.js';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { IStringDictionary } from '../../../base/common/collections.js';
9 > import { getErrorMessage } from '../../../base/common/errors.js';
10 > import { Event } from '../../../base/common/event.js';
11 > import { toFormattedString } from '../../../base/common/jsonFormatter.js';
12 > import { DisposableStore } from '../../../base/common/lifecycle.js';
13 > import { compare } from '../../../base/common/strings.js';
14 > import { URI } from '../../../base/common/uri.js';
15 > import { IConfigurationService } from '../../configuration/common/configuration.js';
16 > import { IEnvironmentService } from '../../environment/common/environment.js';
17 > import { GlobalExtensionEnablementService } from '../../extensionManagement/common/extensionEnablementService.js';
18 > import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, ExtensionInstallSource, EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT } from '../../extensionManagement/common/extensionManagement.js';
19 > import { areSameExtensions } from '../../extensionManagement/common/extensionManagementUtil.js';
20 > import { ExtensionStorageService, IExtensionStorageService } from '../../extensionManagement/common/extensionStorage.js';
21 > import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from '../../extensions/common/extensions.js';
22 > import { IFileService } from '../../files/common/files.js';
23 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
24 > import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
25 > import { ILogService } from '../../log/common/log.js';
26 > import { IStorageService } from '../../storage/common/storage.js';
27 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
28 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
29 > import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
30 > import { AbstractInitializer, AbstractSynchroniser, getSyncResourceLogLabel, IAcceptResult, IMergeResult, IResourcePreview } from './abstractSynchronizer.js';
31 > import { IMergeResult as IExtensionMergeResult, merge } from './extensionsMerge.js';
32 > import { IIgnoredExtensionsManagementService } from './ignoredExtensions.js';
33 > import { Change, IRemoteUserData, ISyncData, ISyncExtension, IUserDataSyncLocalStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME, ILocalSyncExtension } from './userDataSync.js';
34 > import { IUserDataProfileStorageService } from '../../userDataProfile/common/userDataProfileStorageService.js';
35 > import { IProductService } from '../../product/common/productService.js';
36 >
37 > type IExtensionResourceMergeResult = IAcceptResult & IExtensionMergeResult;
38 >
39 > interface IExtensionResourcePreview extends IResourcePreview {
40 > readonly localExtensions: ILocalSyncExtension[];
41 > readonly remoteExtensions: ISyncExtension[] | null;
42 > readonly skippedExtensions: ISyncExtension[];
43 > readonly builtinExtensions: IExtensionIdentifier[] | null;
44 > readonly previewResult: IExtensionResourceMergeResult;
45 > }
46 >
47 > interface ILastSyncUserData extends IRemoteUserData {
48 > skippedExtensions: ISyncExtension[] | undefined;
49 > builtinExtensions: IExtensionIdentifier[] | undefined;
50 > }
51 >
52 async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagementService: IExtensionManagementService): Promise<ISyncExtension[]> {
53 const extensions = JSON.parse(syncData.content);
54 if (syncData.version === 1
55 || syncData.version === 2
56 ) {
57 const builtinExtensions = (await extensionManagementService.getInstalled(ExtensionType.System)).filter(e => e.isBuiltin);
58 for (const extension of extensions) {
59 // #region Migration from v1 (enabled -> disabled)
60 if (syncData.version === 1) {
61 if (extension.enabled === false) {
62 extension.disabled = true;
63 }
64 delete extension.enabled;
65 }
66 // #endregion
67
68 // #region Migration from v2 (set installed property on extension)
69 if (syncData.version === 2) {
70 if (builtinExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) {
71 extension.installed = true;
72 }
73 }
74 // #endregion
75 }
76 }
77 return extensions;
78 }
80 > export function parseExtensions(syncData: ISyncData): ISyncExtension[] {
81 return JSON.parse(syncData.content);
82 }
84 > export function stringify(extensions: ISyncExtension[], format: boolean): string {
85 > extensions.sort((e1, e2) => { extensionsSync.ts ×17
86 if (!e1.identifier.uuid && e2.identifier.uuid) {
87 return -1;
88 }
89 if (e1.identifier.uuid && !e2.identifier.uuid) {
90 return 1;
91 }
92 return compare(e1.identifier.id, e2.identifier.id);
94 > return format ? toFormattedString(extensions, {}) : JSON.stringify(extensions);
95 > }
97 > export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
98 >
99 > /*
100 > Version 3 - Introduce installed property to skip installing built in extensions
101 > protected readonly version: number = 3;
102 > */
103 > /* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
104 > /* Version 5: Introduce extension state */
105 > /* Version 6: Added isApplicationScoped property */
106 > protected readonly version: number = 6;
107 >
108 > private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'extensions.json');
109 > private readonly baseResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
110 > private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
111 > private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
112 > private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
113 >
114 > private readonly localExtensionsProvider: LocalExtensionsProvider;
115 >
116 > constructor(
117 > // profileLocation changes for default profile userDataSyncService.ts ×19
118 > profile: IUserDataProfile,
119 > collection: string | undefined,
120 > @IEnvironmentService environmentService: IEnvironmentService,
121 > @IFileService fileService: IFileService,
122 > @IStorageService storageService: IStorageService,
123 > @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
124 > @IUserDataSyncLocalStoreService userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
125 > @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
126 > @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
127 > @IUserDataSyncLogService logService: IUserDataSyncLogService,
128 > @IConfigurationService configurationService: IConfigurationService,
129 > @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
130 > @ITelemetryService telemetryService: ITelemetryService,
131 > @IExtensionStorageService extensionStorageService: IExtensionStorageService,
132 > @IUriIdentityService uriIdentityService: IUriIdentityService,
133 > @IUserDataProfileStorageService userDataProfileStorageService: IUserDataProfileStorageService,
134 > @IInstantiationService private readonly instantiationService: IInstantiationService,
135 > ) {
136 > super({ syncResource: SyncResource.Extensions, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
137 > this.localExtensionsProvider = this.instantiationService.createInstance(LocalExtensionsProvider);
138 > this._register(
139 > Event.any<any>(
140 > Event.filter(this.extensionManagementService.onDidInstallExtensions, (e => e.some(({ local }) => !!local))),
141 > Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
142 > Event.filter(userDataProfileStorageService.onDidChange, e => e.valueChanges.some(({ profile, changes }) => this.syncResource.profile.id === profile.id && changes.some(change => change.key === DISABLED_EXTENSIONS_STORAGE_PATH))),
143 > extensionStorageService.onDidChangeExtensionStorageToSync)(() => this.triggerLocalChange()));
144 > }
146 > protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
147 > const remoteExtensions = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null; extensionsSync.ts ×17
148 > const skippedExtensions = lastSyncUserData?.skippedExtensions ?? [];
149 > const builtinExtensions = lastSyncUserData?.builtinExtensions ?? null;
150 > const lastSyncExtensions = lastSyncUserData?.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null;
151 >
152 > const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.syncResource.profile);
153 >
154 > if (remoteExtensions) {
155 this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
156 > } else { extensionsSync.ts ×17
157 > this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
158 > }
159 >
160 > const { local, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions, builtinExtensions);
161 > const previewResult: IExtensionResourceMergeResult = {
162 > local, remote,
163 > content: this.getPreviewContent(localExtensions, local.added, local.updated, local.removed),
164 > localChange: local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0 ? Change.Modified : Change.None,
165 > remoteChange: remote !== null ? Change.Modified : Change.None,
166 > };
167 >
168 > const localContent = this.stringify(localExtensions, false);
169 > return [{
170 > skippedExtensions,
171 > builtinExtensions,
172 > baseResource: this.baseResource,
173 > baseContent: lastSyncExtensions ? this.stringify(lastSyncExtensions, false) : localContent,
174 > localResource: this.localResource,
175 > localContent,
176 > localExtensions,
177 > remoteResource: this.remoteResource,
178 > remoteExtensions,
179 > remoteContent: remoteExtensions ? this.stringify(remoteExtensions, false) : null,
180 > previewResource: this.previewResource,
181 > previewResult,
182 > localChange: previewResult.localChange,
183 > remoteChange: previewResult.remoteChange,
184 > acceptedResource: this.acceptedResource,
185 > }];
186 > }
188 > protected async hasRemoteChanged(lastSyncUserData: ILastSyncUserData): Promise<boolean> {
189 const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null;
190 const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.syncResource.profile);
191 const { remote } = merge(localExtensions, lastSyncExtensions, lastSyncExtensions, lastSyncUserData.skippedExtensions || [], ignoredExtensions, lastSyncUserData.builtinExtensions || []);
192 return remote !== null;
193 }
195 > private getPreviewContent(localExtensions: ISyncExtension[], added: ISyncExtension[], updated: ISyncExtension[], removed: IExtensionIdentifier[]): string {
196 > const preview: ISyncExtension[] = [...added, ...updated]; extensionsSync.ts ×17
197 >
198 > const idsOrUUIDs: Set<string> = new Set<string>();
199 > const addIdentifier = (identifier: IExtensionIdentifier) => {
200 idsOrUUIDs.add(identifier.id.toLowerCase());
201 if (identifier.uuid) {
202 idsOrUUIDs.add(identifier.uuid);
203 }
204 };
205 > preview.forEach(({ identifier }) => addIdentifier(identifier)); extensionsSync.ts ×17
206 > removed.forEach(addIdentifier);
207 >
208 > for (const localExtension of localExtensions) {
209 if (idsOrUUIDs.has(localExtension.identifier.id.toLowerCase()) || (localExtension.identifier.uuid && idsOrUUIDs.has(localExtension.identifier.uuid))) {
210 // skip
211 continue;
212 }
213 preview.push(localExtension);
214 }
216 > return this.stringify(preview, false);
217 > }
219 > protected async getMergeResult(resourcePreview: IExtensionResourcePreview, token: CancellationToken): Promise<IMergeResult> {
220 return { ...resourcePreview.previewResult, hasConflicts: false };
221 }
223 > protected async getAcceptResult(resourcePreview: IExtensionResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IExtensionResourceMergeResult> {
224
225 /* Accept local resource */
226 if (this.extUri.isEqual(resource, this.localResource)) {
227 return this.acceptLocal(resourcePreview);
228 }
229
230 /* Accept remote resource */
231 if (this.extUri.isEqual(resource, this.remoteResource)) {
232 return this.acceptRemote(resourcePreview);
233 }
234
235 /* Accept preview resource */
236 if (this.extUri.isEqual(resource, this.previewResource)) {
237 return resourcePreview.previewResult;
238 }
239
240 throw new Error(`Invalid Resource: ${resource.toString()}`);
241 }
243 > private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
244 const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.syncResource.profile.extensionsResource);
245 const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
246 const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
247 const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, remoteExtensions, resourcePreview.skippedExtensions, ignoredExtensions, resourcePreview.builtinExtensions);
248 const { local, remote } = mergeResult;
249 return {
250 content: resourcePreview.localContent,
251 local,
252 remote,
253 localChange: local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0 ? Change.Modified : Change.None,
254 remoteChange: remote !== null ? Change.Modified : Change.None,
255 };
256 }
258 > private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
259 const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.syncResource.profile.extensionsResource);
260 const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
261 const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
262 if (remoteExtensions !== null) {
263 const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions, resourcePreview.builtinExtensions);
264 const { local, remote } = mergeResult;
265 return {
266 content: resourcePreview.remoteContent,
267 local,
268 remote,
269 localChange: local.added.length > 0 || local.removed.length > 0 || local.updated.length > 0 ? Change.Modified : Change.None,
270 remoteChange: remote !== null ? Change.Modified : Change.None,
271 };
272 } else {
273 return {
274 content: resourcePreview.remoteContent,
275 local: { added: [], removed: [], updated: [] },
276 remote: null,
277 localChange: Change.None,
278 remoteChange: Change.None,
279 };
280 }
281 }
283 > protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IExtensionResourcePreview, IExtensionResourceMergeResult][], force: boolean): Promise<void> {
284 > let { skippedExtensions, builtinExtensions, localExtensions } = resourcePreviews[0][0]; extensionsSync.ts ×17
285 > const { local, remote, localChange, remoteChange } = resourcePreviews[0][1];
286 >
287 > if (localChange === Change.None && remoteChange === Change.None) {
288 > this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
289 > }
290 >
291 > if (localChange !== Change.None) {
292 await this.backupLocal(JSON.stringify(localExtensions));
293 skippedExtensions = await this.localExtensionsProvider.updateLocalExtensions(local.added, local.removed, local.updated, skippedExtensions, this.syncResource.profile);
294 }
296 > if (remote) {
297 // update remote
298 this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
299 const content = JSON.stringify(remote.all);
300 remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
301 this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions.${remote.added.length ? ` Added: ${JSON.stringify(remote.added.map(e => e.identifier.id))}.` : ''}${remote.updated.length ? ` Updated: ${JSON.stringify(remote.updated.map(e => e.identifier.id))}.` : ''}${remote.removed.length ? ` Removed: ${JSON.stringify(remote.removed.map(e => e.identifier.id))}.` : ''}`);
302 }
304 > if (lastSyncUserData?.ref !== remoteUserData.ref) {
305 > // update last sync
306 > this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`);
307 > builtinExtensions = this.computeBuiltinExtensions(localExtensions, builtinExtensions);
308 > await this.updateLastSyncUserData(remoteUserData, { skippedExtensions, builtinExtensions });
309 > this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions.${skippedExtensions.length ? ` Skipped: ${JSON.stringify(skippedExtensions.map(e => e.identifier.id))}.` : ''}`);
310 > }
311 > }
313 > private computeBuiltinExtensions(localExtensions: ILocalSyncExtension[], previousBuiltinExtensions: IExtensionIdentifier[] | null): IExtensionIdentifier[] {
314 > const localExtensionsSet = new Set<string>(); extensionsSync.ts ×17
315 > const builtinExtensions: IExtensionIdentifier[] = [];
316 > for (const localExtension of localExtensions) {
317 localExtensionsSet.add(localExtension.identifier.id.toLowerCase());
318 if (!localExtension.installed) {
319 builtinExtensions.push(localExtension.identifier);
320 }
321 }
322 > if (previousBuiltinExtensions) { extensionsSync.ts ×17
323 for (const builtinExtension of previousBuiltinExtensions) {
324 // Add previous builtin extension if it does not exist in local extensions
325 if (!localExtensionsSet.has(builtinExtension.id.toLowerCase())) {
326 builtinExtensions.push(builtinExtension);
327 }
328 }
329 }
330 > return builtinExtensions; extensionsSync.ts ×17
331 > }
333 > async resolveContent(uri: URI): Promise<string | null> {
334 if (this.extUri.isEqual(this.remoteResource, uri)
335 || this.extUri.isEqual(this.baseResource, uri)
336 || this.extUri.isEqual(this.localResource, uri)
337 || this.extUri.isEqual(this.acceptedResource, uri)
338 ) {
339 const content = await this.resolvePreviewContent(uri);
340 return content ? this.stringify(JSON.parse(content), true) : content;
341 }
342 return null;
343 }
345 > private stringify(extensions: ISyncExtension[], format: boolean): string {
346 > return stringify(extensions, format); extensionsSync.ts ×17
347 > }
349 > async hasLocalData(): Promise<boolean> {
350 try {
351 const { localExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.syncResource.profile);
352 if (localExtensions.some(e => e.installed || e.disabled)) {
353 return true;
354 }
355 } catch (error) {
356 /* ignore error */
357 }
358 return false;
359 }
361 > }
362 >
363 > export class LocalExtensionsProvider {
364 >
365 > constructor(
366 > @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, userDataSyncService.ts ×19
367 > @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService,
368 > @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
369 > @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
370 > @IInstantiationService private readonly instantiationService: IInstantiationService,
371 > @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
372 > @IProductService private readonly productService: IProductService,
373 > ) { }
375 > async getLocalExtensions(profile: IUserDataProfile): Promise<{ localExtensions: ILocalSyncExtension[]; ignoredExtensions: string[] }> {
376 > const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); extensionsSync.ts ×17
377 > const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
378 > const localExtensions = await this.withProfileScopedServices(profile, async (extensionEnablementService, extensionStorageService) => {
379 > const disabledExtensions = extensionEnablementService.getDisabledExtensions();
380 > return installedExtensions
381 > .map(extension => {
382 const { identifier, isBuiltin, manifest, preRelease, pinned, isApplicationScoped } = extension;
383 const syncExtension: ILocalSyncExtension = { identifier, preRelease, version: manifest.version, pinned: !!pinned };
384 if (isApplicationScoped && !isApplicationScopedExtension(manifest)) {
385 syncExtension.isApplicationScoped = isApplicationScoped;
386 }
387 if (this.productService.builtInExtensionsEnabledWithAutoUpdates?.some(id => id.toLowerCase() === identifier.id.toLowerCase())) {
388 syncExtension.isApplicationScoped = true;
389 }
390 if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
391 syncExtension.disabled = true;
392 }
393 if (!isBuiltin) {
394 syncExtension.installed = true;
395 }
396 try {
397 const keys = extensionStorageService.getKeysForSync({ id: identifier.id, version: manifest.version });
398 if (keys) {
399 const extensionStorageState = extensionStorageService.getExtensionState(extension, true) || {};
400 syncExtension.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary<any>, key) => {
401 if (keys.includes(key)) {
402 state[key] = extensionStorageState[key];
403 }
404 return state;
405 }, {});
406 }
407 } catch (error) {
408 this.logService.info(`${getSyncResourceLogLabel(SyncResource.Extensions, profile)}: Error while parsing extension state`, getErrorMessage(error));
409 }
410 return syncExtension;
412 > });
413 > return { localExtensions, ignoredExtensions };
414 > }
416 > async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[], profile: IUserDataProfile): Promise<ISyncExtension[]> {
417 const syncResourceLogLabel = getSyncResourceLogLabel(SyncResource.Extensions, profile);
418 const extensionsToInstall: InstallExtensionInfo[] = [];
419 const syncExtensionsToInstall = new Map<string, ISyncExtension>();
420 const removeFromSkipped: IExtensionIdentifier[] = [];
421 const addToSkipped: ISyncExtension[] = [];
422 const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource);
423
424 // 1. Sync extensions state first so that the storage is flushed and updated in all opened windows
425 if (added.length || updated.length) {
426 await this.withProfileScopedServices(profile, async (extensionEnablementService, extensionStorageService) => {
427 await Promises.settled([...added, ...updated].map(async e => {
428 const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier));
429
430 // Builtin Extension Sync: Enablement & State
431 if (installedExtension && installedExtension.isBuiltin) {
432 if (e.state && installedExtension.manifest.version === e.version) {
433 this.updateExtensionState(e.state, installedExtension, installedExtension.manifest.version, extensionStorageService);
434 }
435 const isDisabled = extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier));
436 if (isDisabled !== !!e.disabled) {
437 if (e.disabled) {
438 this.logService.trace(`${syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
439 await extensionEnablementService.disableExtension(e.identifier);
440 this.logService.info(`${syncResourceLogLabel}: Disabled extension`, e.identifier.id);
441 } else {
442 this.logService.trace(`${syncResourceLogLabel}: Enabling extension...`, e.identifier.id);
443 await extensionEnablementService.enableExtension(e.identifier);
444 this.logService.info(`${syncResourceLogLabel}: Enabled extension`, e.identifier.id);
445 }
446 }
447 removeFromSkipped.push(e.identifier);
448 return;
449 }
450
451 // User Extension Sync: Install/Update, Enablement & State
452 const version = e.pinned ? e.version : undefined;
453 const extension = (await this.extensionGalleryService.getExtensions([{ ...e.identifier, version, preRelease: version ? undefined : e.preRelease }], CancellationToken.None))[0];
454
455 /* Update extension state only if
456 * extension is installed and version is same as synced version or
457 * extension is not installed and installable
458 */
459 if (e.state &&
460 (installedExtension ? installedExtension.manifest.version === e.version /* Installed and remote has same version */
461 : !!extension /* Installable */)
462 ) {
463 this.updateExtensionState(e.state, installedExtension || extension, installedExtension?.manifest.version, extensionStorageService);
464 }
465
466 if (extension) {
467 try {
468 const isDisabled = extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier));
469 if (isDisabled !== !!e.disabled) {
470 if (e.disabled) {
471 this.logService.trace(`${syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
472 await extensionEnablementService.disableExtension(extension.identifier);
473 this.logService.info(`${syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version);
474 } else {
475 this.logService.trace(`${syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version);
476 await extensionEnablementService.enableExtension(extension.identifier);
477 this.logService.info(`${syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
478 }
479 }
480
481 if (!installedExtension // Install if the extension does not exist
482 || installedExtension.preRelease !== e.preRelease // Install if the extension pre-release preference has changed
483 || installedExtension.pinned !== e.pinned // Install if the extension pinned preference has changed
484 || (version && installedExtension.manifest.version !== version) // Install if the extension version has changed
485 ) {
486 if (await this.extensionManagementService.canInstall(extension) === true) {
487 extensionsToInstall.push({
488 extension, options: {
489 isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */,
490 donotIncludePackAndDependencies: true,
491 installGivenVersion: e.pinned && !!e.version,
492 pinned: e.pinned,
493 installPreReleaseVersion: e.preRelease,
494 preRelease: e.preRelease,
495 profileLocation: profile.extensionsResource,
496 isApplicationScoped: e.isApplicationScoped,
497 context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC, [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true }
498 }
499 });
500 syncExtensionsToInstall.set(extension.identifier.id.toLowerCase(), e);
501 } else {
502 this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because it cannot be installed.`, extension.displayName || extension.identifier.id);
503 addToSkipped.push(e);
504 }
505 }
506 } catch (error) {
507 addToSkipped.push(e);
508 this.logService.error(error);
509 this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id);
510 }
511 } else {
512 addToSkipped.push(e);
513 this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the extension is not found.`, e.identifier.id);
514 }
515 }));
516 });
517 }
518
519 // 2. Next uninstall the removed extensions
520 if (removed.length) {
521 const extensionsToRemove = installedExtensions.filter(({ identifier, isBuiltin }) => !isBuiltin && removed.some(r => areSameExtensions(identifier, r)));
522 await Promises.settled(extensionsToRemove.map(async extensionToRemove => {
523 this.logService.trace(`${syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id);
524 await this.extensionManagementService.uninstall(extensionToRemove, { donotIncludePack: true, donotCheckDependents: true, profileLocation: profile.extensionsResource });
525 this.logService.info(`${syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id);
526 removeFromSkipped.push(extensionToRemove.identifier);
527 }));
528 }
529
530 // 3. Install extensions at the end
531 const results = await this.extensionManagementService.installGalleryExtensions(extensionsToInstall);
532 for (const { identifier, local, error, source } of results) {
533 const gallery = source as IGalleryExtension;
534 if (local) {
535 this.logService.info(`${syncResourceLogLabel}: Installed extension.`, identifier.id, gallery.version);
536 removeFromSkipped.push(identifier);
537 } else {
538 const e = syncExtensionsToInstall.get(identifier.id.toLowerCase());
539 if (e) {
540 addToSkipped.push(e);
541 this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id);
542 }
543 if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) {
544 this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id);
545 } else if (error) {
546 this.logService.error(error);
547 }
548 }
549 }
550
551 const newSkippedExtensions: ISyncExtension[] = [];
552 for (const skippedExtension of skippedExtensions) {
553 if (!removeFromSkipped.some(e => areSameExtensions(e, skippedExtension.identifier))) {
554 newSkippedExtensions.push(skippedExtension);
555 }
556 }
557 for (const skippedExtension of addToSkipped) {
558 if (!newSkippedExtensions.some(e => areSameExtensions(e.identifier, skippedExtension.identifier))) {
559 newSkippedExtensions.push(skippedExtension);
560 }
561 }
562 return newSkippedExtensions;
563 }
565 > private updateExtensionState(state: IStringDictionary<any>, extension: ILocalExtension | IGalleryExtension, version: string | undefined, extensionStorageService: IExtensionStorageService): void {
566 const extensionState = extensionStorageService.getExtensionState(extension, true) || {};
567 const keys = version ? extensionStorageService.getKeysForSync({ id: extension.identifier.id, version }) : undefined;
568 if (keys) {
569 keys.forEach(key => { extensionState[key] = state[key]; });
570 } else {
571 Object.keys(state).forEach(key => extensionState[key] = state[key]);
572 }
573 extensionStorageService.setExtensionState(extension, extensionState, true);
574 }
576 > private async withProfileScopedServices<T>(profile: IUserDataProfile, fn: (extensionEnablementService: IGlobalExtensionEnablementService, extensionStorageService: IExtensionStorageService) => Promise<T>): Promise<T> {
577 > return this.userDataProfileStorageService.withProfileScopedStorageService(profile, extensionsSync.ts ×17
578 > async storageService => {
579 > const disposables = new DisposableStore();
580 > const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])));
581 > const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService));
582 > const extensionStorageService = disposables.add(instantiationService.createInstance(ExtensionStorageService));
583 > try {
584 > return await fn(extensionEnablementService, extensionStorageService);
585 > } finally {
586 > disposables.dispose();
587 > }
588 > });
589 > }
591 > }
592 >
593 > export interface IExtensionsInitializerPreviewResult {
594 > readonly installedExtensions: ILocalExtension[];
595 > readonly disabledExtensions: IExtensionIdentifier[];
596 > readonly newExtensions: (IExtensionIdentifier & { preRelease: boolean })[];
597 > readonly remoteExtensions: ISyncExtension[];
598 > }
599 >
600 > export abstract class AbstractExtensionsInitializer extends AbstractInitializer {
601 >
602 > constructor(
603 @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService,
604 @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
605 @IFileService fileService: IFileService,
606 @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService,
607 @IEnvironmentService environmentService: IEnvironmentService,
608 @ILogService logService: ILogService,
609 @IStorageService storageService: IStorageService,
610 @IUriIdentityService uriIdentityService: IUriIdentityService,
611 ) {
612 super(SyncResource.Extensions, userDataProfilesService, environmentService, logService, fileService, storageService, uriIdentityService);
613 }
615 > protected async parseExtensions(remoteUserData: IRemoteUserData): Promise<ISyncExtension[] | null> {
616 return remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null;
617 }
619 > protected generatePreview(remoteExtensions: ISyncExtension[], localExtensions: ILocalExtension[]): IExtensionsInitializerPreviewResult {
620 const installedExtensions: ILocalExtension[] = [];
621 const newExtensions: (IExtensionIdentifier & { preRelease: boolean })[] = [];
622 const disabledExtensions: IExtensionIdentifier[] = [];
623 for (const extension of remoteExtensions) {
624 if (this.ignoredExtensionsManagementService.hasToNeverSyncExtension(extension.identifier.id)) {
625 // Skip extension ignored to sync
626 continue;
627 }
628
629 const installedExtension = localExtensions.find(i => areSameExtensions(i.identifier, extension.identifier));
630 if (installedExtension) {
631 installedExtensions.push(installedExtension);
632 if (extension.disabled) {
633 disabledExtensions.push(extension.identifier);
634 }
635 } else if (extension.installed) {
636 newExtensions.push({ ...extension.identifier, preRelease: !!extension.preRelease });
637 if (extension.disabled) {
638 disabledExtensions.push(extension.identifier);
639 }
640 }
641 }
642 return { installedExtensions, newExtensions, disabledExtensions, remoteExtensions };
643 }
645 > }