@@ -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
7172type 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 ) ;
0 commit comments