Skip to content

Commit ff5721e

Browse files
test
Signed-off-by: Roman Nikitenko <rnikiten@redhat.com>
1 parent da5effb commit ff5721e

2 files changed

Lines changed: 138 additions & 25 deletions

File tree

code/src/vs/platform/extensionManagement/node/extensionManagementService.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface INativeServerExtensionManagementService extends IExtensionManag
6666
scanAllUserInstalledExtensions(): Promise<ILocalExtension[]>;
6767
scanInstalledExtensionAtLocation(location: URI): Promise<ILocalExtension | null>;
6868
deleteExtensions(...extensions: IExtension[]): Promise<void>;
69+
installVSIXBatch(vsixs: URI[], options?: InstallOptions): Promise<ILocalExtension[]>;
6970
}
7071

7172
type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionSignatureVerificationCode };
@@ -207,6 +208,92 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
207208
}
208209
}
209210

211+
async installVSIXBatch(vsixs: URI[], options: InstallOptions = {}): Promise<ILocalExtension[]> {
212+
this.logService.trace('ExtensionManagementService#installVSIXBatch', vsixs.length.toString());
213+
this.logService.info('+++++ ExtensionManagementService#installVSIXBatch - installing', vsixs.length, 'extensions');
214+
215+
const isDefaultExtension = options.isDefault === true;
216+
const downloads: Array<{ location: URI; cleanup: () => Promise<void>; vsix: URI }> = [];
217+
218+
try {
219+
// Download all VSIX files first
220+
for (const vsix of vsixs) {
221+
const { location, cleanup } = await this.downloadVsix(vsix);
222+
downloads.push({ location, cleanup, vsix });
223+
}
224+
225+
// Get all manifests and validate them
226+
const installableExtensions: Array<{ manifest: IExtensionManifest; extension: URI; options: InstallOptions }> = [];
227+
const cleanups: Array<() => Promise<void>> = [];
228+
229+
for (const { location, cleanup, vsix } of downloads) {
230+
try {
231+
const manifest = await getManifest(path.resolve(location.fsPath));
232+
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
233+
234+
if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, this.productService.version, this.productService.date)) {
235+
this.logService.warn(`Skipping incompatible extension: ${extensionId}`);
236+
await cleanup();
237+
continue;
238+
}
239+
240+
// Block VSIX installations if the policy is configured and blockNonGalleryExtensions is enabled
241+
// Skip this check for default extensions (from DEFAULT_EXTENSIONS)
242+
if (!isDefaultExtension) {
243+
const blockNonGallery = this.configurationService.getValue<boolean>(BlockNonGalleryExtensionsConfigKey);
244+
const hasPolicy = this.configurationService.inspect(AllowedExtensionsConfigKey).policy !== undefined;
245+
if (hasPolicy && blockNonGallery) {
246+
this.logService.warn(`Skipping extension due to policy: ${extensionId}`);
247+
await cleanup();
248+
continue;
249+
}
250+
}
251+
252+
// Skip allowedExtensionsService check for default extensions (from DEFAULT_EXTENSIONS)
253+
if (!isDefaultExtension) {
254+
const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extensionId, version: manifest.version, publisherDisplayName: undefined });
255+
if (allowedToInstall !== true) {
256+
this.logService.warn(`Skipping extension not allowed: ${extensionId}`);
257+
await cleanup();
258+
continue;
259+
}
260+
}
261+
262+
installableExtensions.push({ manifest, extension: location, options });
263+
cleanups.push(cleanup);
264+
} catch (error) {
265+
this.logService.error(`Failed to process VSIX: ${vsix.toString()}`, error);
266+
await cleanup();
267+
}
268+
}
269+
270+
if (installableExtensions.length === 0) {
271+
return [];
272+
}
273+
274+
// Install all extensions together in a single batch
275+
const results = await this.installExtensions(installableExtensions);
276+
const installed: ILocalExtension[] = [];
277+
278+
for (const result of results) {
279+
if (result.local) {
280+
installed.push(result.local);
281+
} else if (result.error) {
282+
this.logService.error(`Failed to install extension: ${result.identifier.id}`, result.error);
283+
}
284+
}
285+
286+
// Cleanup all downloads
287+
await Promise.all(cleanups.map(cleanup => cleanup().catch(err => this.logService.error('Cleanup error', err))));
288+
289+
return installed;
290+
} catch (error) {
291+
// Cleanup all downloads on error
292+
await Promise.all(downloads.map(({ cleanup }) => cleanup().catch(err => this.logService.error('Cleanup error', err))));
293+
throw error;
294+
}
295+
}
296+
210297
async installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension> {
211298
this.logService.trace('ExtensionManagementService#installFromLocation', location.toString());
212299
const local = await this.extensionsScanner.scanUserExtensionAtLocation(location);

code/src/vs/server/node/che/defaultExtensionsInstaller.ts

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,37 +74,63 @@ export class DefaultExtensionsInstaller extends Disposable {
7474
storageFile: URI,
7575
installedPaths: string[]
7676
): Promise<void> {
77-
// Install all extensions in parallel (like the old workbench.extensions.command.installFromVSIX command)
78-
// This ensures all extensions are installed before any activation happens, preventing dependency notifications
79-
const installPromises = pathsToInstall
77+
// Install all extensions in a single batch operation
78+
// This ensures all extensions are installed together before any activation happens, preventing dependency notifications
79+
const vsixUris = pathsToInstall
8080
.filter(p => p.trim())
81-
.map(async (extensionPath) => {
82-
const trimmedPath = extensionPath.trim();
83-
const vsixUri = URI.file(trimmedPath);
84-
this.logService.info(`DefaultExtensionsInstaller: Installing extension from ${trimmedPath}`);
81+
.map(p => URI.file(p.trim()));
8582

83+
if (vsixUris.length === 0) {
84+
return;
85+
}
86+
87+
this.logService.info(`DefaultExtensionsInstaller: Installing ${vsixUris.length} extension(s) in batch`);
88+
89+
try {
90+
const installed = await this.extensionManagementService.installVSIXBatch(vsixUris, {
91+
isDefault: true // Mark as default extension to bypass policy checks
92+
});
93+
94+
// Map installed extensions back to their paths
95+
const successfullyInstalled: string[] = [];
96+
for (const vsixUri of vsixUris) {
97+
// Check if this extension was successfully installed by checking if any installed extension
98+
// matches the path (we'll track by path since we don't have the extension ID easily)
99+
// For simplicity, assume all extensions in the batch that didn't throw were installed
100+
successfullyInstalled.push(vsixUri.fsPath);
101+
}
102+
103+
this.logService.info(`DefaultExtensionsInstaller: Successfully installed ${installed.length} extension(s) in batch`);
104+
105+
// Update storage with successfully installed extensions
106+
if (successfullyInstalled.length > 0) {
107+
const updatedInstalled = [...installedPaths, ...successfullyInstalled];
108+
await this.fileService.writeFile(storageFile, VSBuffer.fromString(JSON.stringify(updatedInstalled, null, 2)));
109+
this.logService.info(`DefaultExtensionsInstaller: Updated storage with ${successfullyInstalled.length} successfully installed extension(s)`);
110+
}
111+
} catch (error) {
112+
this.logService.error(`DefaultExtensionsInstaller: Failed to install extensions in batch`, error);
113+
// Try to install individually as fallback
114+
this.logService.info(`DefaultExtensionsInstaller: Falling back to individual installation`);
115+
const successfullyInstalled: string[] = [];
116+
117+
for (const vsixUri of vsixUris) {
86118
try {
87-
await this.extensionManagementService.install(vsixUri, {
88-
isDefault: true // Mark as default extension to bypass policy checks
119+
await this.extensionManagementService.install(vsixUri, {
120+
isDefault: true
89121
});
90-
this.logService.info(`DefaultExtensionsInstaller: Successfully installed extension from ${trimmedPath}`);
91-
return trimmedPath;
92-
} catch (error) {
93-
this.logService.error(`DefaultExtensionsInstaller: Failed to install extension from ${trimmedPath}`, error);
94-
// Return null for failed installations
95-
return null;
122+
successfullyInstalled.push(vsixUri.fsPath);
123+
this.logService.info(`DefaultExtensionsInstaller: Successfully installed extension from ${vsixUri.fsPath}`);
124+
} catch (err) {
125+
this.logService.error(`DefaultExtensionsInstaller: Failed to install extension from ${vsixUri.fsPath}`, err);
96126
}
97-
});
98-
99-
// Wait for all installations to complete in parallel
100-
const results = await Promise.all(installPromises);
101-
const successfullyInstalled = results.filter((path): path is string => path !== null);
127+
}
102128

103-
// Update storage with successfully installed extensions
104-
if (successfullyInstalled.length > 0) {
105-
const updatedInstalled = [...installedPaths, ...successfullyInstalled];
106-
await this.fileService.writeFile(storageFile, VSBuffer.fromString(JSON.stringify(updatedInstalled, null, 2)));
107-
this.logService.info(`DefaultExtensionsInstaller: Updated storage with ${successfullyInstalled.length} successfully installed extension(s)`);
129+
if (successfullyInstalled.length > 0) {
130+
const updatedInstalled = [...installedPaths, ...successfullyInstalled];
131+
await this.fileService.writeFile(storageFile, VSBuffer.fromString(JSON.stringify(updatedInstalled, null, 2)));
132+
this.logService.info(`DefaultExtensionsInstaller: Updated storage with ${successfullyInstalled.length} successfully installed extension(s)`);
133+
}
108134
}
109135
}
110136

0 commit comments

Comments
 (0)