Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/common/extensionStorage.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
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 { createDecorator } from '../../instantiation/common/instantiation.js';
7
import { Emitter, Event } from '../../../base/common/event.js';
8
import { Disposable } from '../../../base/common/lifecycle.js';
9
import { IProfileStorageValueChangeEvent, IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
10
import { adoptToGalleryExtensionId, areSameExtensions, getExtensionId } from './extensionManagementUtil.js';
11
import { IProductService } from '../../product/common/productService.js';
12
import { distinct } from '../../../base/common/arrays.js';
13
import { ILogService } from '../../log/common/log.js';
14
import { IExtension } from '../../extensions/common/extensions.js';
15
import { isString } from '../../../base/common/types.js';
16
import { IStringDictionary } from '../../../base/common/collections.js';
17
import { IExtensionManagementService, IGalleryExtension } from './extensionManagement.js';
18
19
export interface IExtensionIdWithVersion {
20
id: string;
21
version: string;
22
}
23
24
export const IExtensionStorageService = createDecorator<IExtensionStorageService>('IExtensionStorageService');
25
26
export interface IExtensionStorageService {
27
readonly _serviceBrand: undefined;
28
29
getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined;
30
getExtensionStateRaw(extension: IExtension | IGalleryExtension | string, global: boolean): string | undefined;
31
setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void;
32
33
readonly onDidChangeExtensionStorageToSync: Event<void>;
34
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void;
35
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined;
36
37
addToMigrationList(from: string, to: string): void;
38
getSourceExtensionToMigrate(target: string): string | undefined;
39
}
40
41
const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
42
43
export class ExtensionStorageService extends Disposable implements IExtensionStorageService {
44
45
readonly _serviceBrand: undefined;
46
47
private static LARGE_STATE_WARNING_THRESHOLD = 512 * 1024;
48
49
private static toKey(extension: IExtensionIdWithVersion): string {
50
return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`;
51
}
52
53
private static fromKey(key: string): IExtensionIdWithVersion | undefined {
54
const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key);
55
if (matches && matches[1]) {
56
return { id: matches[1], version: matches[2] };
57
}
58
return undefined;
59
}
60
61
/* TODO @sandy081: This has to be done across all profiles */
62
static async removeOutdatedExtensionVersions(extensionManagementService: IExtensionManagementService, storageService: IStorageService): Promise<void> {
63
const extensions = await extensionManagementService.getInstalled();
64
const extensionVersionsToRemove: string[] = [];
65
for (const [id, versions] of ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService)) {
66
const extensionVersion = extensions.find(e => areSameExtensions(e.identifier, { id }))?.manifest.version;
67
for (const version of versions) {
68
if (extensionVersion !== version) {
69
extensionVersionsToRemove.push(ExtensionStorageService.toKey({ id, version }));
70
}
71
}
72
}
73
for (const key of extensionVersionsToRemove) {
74
storageService.remove(key, StorageScope.PROFILE);
75
}
76
}
77
78
private static readAllExtensionsWithKeysForSync(storageService: IStorageService): Map<string, string[]> {
79
const extensionsWithKeysForSync = new Map<string, string[]>();
80
const keys = storageService.keys(StorageScope.PROFILE, StorageTarget.MACHINE);
81
for (const key of keys) {
82
const extensionIdWithVersion = ExtensionStorageService.fromKey(key);
83
if (extensionIdWithVersion) {
84
let versions = extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());
85
if (!versions) {
86
extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);
87
}
88
versions.push(extensionIdWithVersion.version);
89
}
90
}
91
return extensionsWithKeysForSync;
92
}
93
94
private readonly _onDidChangeExtensionStorageToSync = this._register(new Emitter<void>());
95
readonly onDidChangeExtensionStorageToSync = this._onDidChangeExtensionStorageToSync.event;
96
97
private readonly extensionsWithKeysForSync: Map<string, string[]>;
98
99
constructor(
100
@IStorageService private readonly storageService: IStorageService,
101
@IProductService private readonly productService: IProductService,
102
@ILogService private readonly logService: ILogService,
103
) {
104
super();
105
this.extensionsWithKeysForSync = ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService);
106
this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, undefined, this._store)(e => this.onDidChangeStorageValue(e)));
107
}
108
109
private onDidChangeStorageValue(e: IProfileStorageValueChangeEvent): void {
110
111
// State of extension with keys for sync has changed
112
if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) {
113
this._onDidChangeExtensionStorageToSync.fire();
114
return;
115
}
116
117
// Keys for sync of an extension has changed
118
const extensionIdWithVersion = ExtensionStorageService.fromKey(e.key);
119
if (extensionIdWithVersion) {
120
if (this.storageService.get(e.key, StorageScope.PROFILE) === undefined) {
121
this.extensionsWithKeysForSync.delete(extensionIdWithVersion.id.toLowerCase());
122
} else {
123
let versions = this.extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());
124
if (!versions) {
125
this.extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);
126
}
127
versions.push(extensionIdWithVersion.version);
128
this._onDidChangeExtensionStorageToSync.fire();
129
}
130
return;
131
}
132
}
133
134
private getExtensionId(extension: IExtension | IGalleryExtension | string): string {
135
if (isString(extension)) {
136
return extension;
137
}
138
const publisher = (extension as IExtension).manifest ? (extension as IExtension).manifest.publisher : (extension as IGalleryExtension).publisher;
139
const name = (extension as IExtension).manifest ? (extension as IExtension).manifest.name : (extension as IGalleryExtension).name;
140
return getExtensionId(publisher, name);
141
}
142
143
getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined {
144
const extensionId = this.getExtensionId(extension);
145
const jsonValue = this.getExtensionStateRaw(extension, global);
146
if (jsonValue) {
147
try {
148
return JSON.parse(jsonValue);
149
} catch (error) {
150
// Do not fail this call but log it for diagnostics
151
// https://github.com/microsoft/vscode/issues/132777
152
this.logService.error(`[mainThreadStorage] unexpected error parsing storage contents (extensionId: ${extensionId}, global: ${global}): ${error}`);
153
}
154
}
155
156
return undefined;
157
}
158
159
getExtensionStateRaw(extension: IExtension | IGalleryExtension | string, global: boolean): string | undefined {
160
const extensionId = this.getExtensionId(extension);
161
const rawState = this.storageService.get(extensionId, global ? StorageScope.PROFILE : StorageScope.WORKSPACE);
162
163
if (rawState && rawState?.length > ExtensionStorageService.LARGE_STATE_WARNING_THRESHOLD) {
164
this.logService.warn(`[mainThreadStorage] large extension state detected (extensionId: ${extensionId}, global: ${global}): ${rawState.length / 1024}kb. Consider to use 'storageUri' or 'globalStorageUri' to store this data on disk instead.`);
165
}
166
167
return rawState;
168
}
169
170
setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void {
171
const extensionId = this.getExtensionId(extension);
172
if (state === undefined) {
173
this.storageService.remove(extensionId, global ? StorageScope.PROFILE : StorageScope.WORKSPACE);
174
} else {
175
this.storageService.store(extensionId, JSON.stringify(state), global ? StorageScope.PROFILE : StorageScope.WORKSPACE, StorageTarget.MACHINE /* Extension state is synced separately through extensions */);
176
}
177
}
178
179
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void {
180
this.storageService.store(ExtensionStorageService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.PROFILE, StorageTarget.MACHINE);
181
}
182
183
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined {
184
const extensionKeysForSyncFromProduct = this.productService.extensionSyncedKeys?.[extensionIdWithVersion.id.toLowerCase()];
185
const extensionKeysForSyncFromStorageValue = this.storageService.get(ExtensionStorageService.toKey(extensionIdWithVersion), StorageScope.PROFILE);
186
const extensionKeysForSyncFromStorage = extensionKeysForSyncFromStorageValue ? JSON.parse(extensionKeysForSyncFromStorageValue) : undefined;
187
188
return extensionKeysForSyncFromStorage && extensionKeysForSyncFromProduct
189
? distinct([...extensionKeysForSyncFromStorage, ...extensionKeysForSyncFromProduct])
190
: (extensionKeysForSyncFromStorage || extensionKeysForSyncFromProduct);
191
}
192
193
addToMigrationList(from: string, to: string): void {
194
if (from !== to) {
195
// remove the duplicates
196
const migrationList: [string, string][] = this.migrationList.filter(entry => !entry.includes(from) && !entry.includes(to));
197
migrationList.push([from, to]);
198
this.migrationList = migrationList;
199
}
200
}
201
202
getSourceExtensionToMigrate(toExtensionId: string): string | undefined {
203
const entry = this.migrationList.find(([, to]) => toExtensionId === to);
204
return entry ? entry[0] : undefined;
205
}
206
207
private get migrationList(): [string, string][] {
208
const value = this.storageService.get('extensionStorage.migrationList', StorageScope.APPLICATION, '[]');
209
try {
210
const migrationList = JSON.parse(value);
211
if (Array.isArray(migrationList)) {
212
return migrationList;
213
}
214
} catch (error) { /* ignore */ }
215
return [];
216
}
217
218
private set migrationList(migrationList: [string, string][]) {
219
if (migrationList.length) {
220
this.storageService.store('extensionStorage.migrationList', JSON.stringify(migrationList), StorageScope.APPLICATION, StorageTarget.MACHINE);
221
} else {
222
this.storageService.remove('extensionStorage.migrationList', StorageScope.APPLICATION);
223
}
224
}
225
226
}
227
228