@@ -10,6 +10,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
1010import { toErrorMessage } from 'vs/base/common/errorMessage' ;
1111import { getErrorMessage } from 'vs/base/common/errors' ;
1212import { Emitter } from 'vs/base/common/event' ;
13+ import { hash } from 'vs/base/common/hash' ;
1314import { Disposable } from 'vs/base/common/lifecycle' ;
1415import { ResourceSet } from 'vs/base/common/map' ;
1516import { Schemas } from 'vs/base/common/network' ;
@@ -19,7 +20,7 @@ import { joinPath } from 'vs/base/common/resources';
1920import * as semver from 'vs/base/common/semver/semver' ;
2021import { isBoolean , isUndefined } from 'vs/base/common/types' ;
2122import { URI } from 'vs/base/common/uri' ;
22- import { generateUuid , isUUID } from 'vs/base/common/uuid' ;
23+ import { generateUuid } from 'vs/base/common/uuid' ;
2324import * as pfs from 'vs/base/node/pfs' ;
2425import { extract , ExtractError , IFile , zip } from 'vs/base/node/zip' ;
2526import * as nls from 'vs/nls' ;
@@ -62,6 +63,8 @@ export interface INativeServerExtensionManagementService extends IExtensionManag
6263 markAsUninstalled ( ...extensions : IExtension [ ] ) : Promise < void > ;
6364}
6465
66+ const DELETED_FOLDER_POSTFIX = '.vsctmp' ;
67+
6568export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService {
6669
6770 private readonly extensionsScanner : ExtensionsScanner ;
@@ -417,14 +420,11 @@ export class ExtensionsScanner extends Disposable {
417420 private readonly _onExtract = this . _register ( new Emitter < URI > ( ) ) ;
418421 readonly onExtract = this . _onExtract . event ;
419422
420- private cleanUpGeneratedFoldersPromise : Promise < void > = Promise . resolve ( ) ;
421-
422423 constructor (
423424 private readonly beforeRemovingExtension : ( e : ILocalExtension ) => Promise < void > ,
424425 @IFileService private readonly fileService : IFileService ,
425426 @IExtensionsScannerService private readonly extensionsScannerService : IExtensionsScannerService ,
426427 @IExtensionsProfileScannerService private readonly extensionsProfileScannerService : IExtensionsProfileScannerService ,
427- @IUriIdentityService private readonly uriIdentityService : IUriIdentityService ,
428428 @ILogService private readonly logService : ILogService ,
429429 ) {
430430 super ( ) ;
@@ -433,9 +433,8 @@ export class ExtensionsScanner extends Disposable {
433433 }
434434
435435 async cleanUp ( ) : Promise < void > {
436+ await this . removeTemporarilyDeletedFolders ( ) ;
436437 await this . removeUninstalledExtensions ( ) ;
437- this . cleanUpGeneratedFoldersPromise = this . cleanUpGeneratedFoldersPromise . then ( ( ) => this . removeGeneratedFolders ( ) ) ;
438- await this . cleanUpGeneratedFoldersPromise ;
439438 }
440439
441440 async scanExtensions ( type : ExtensionType | null , profileLocation : URI ) : Promise < ILocalExtension [ ] > {
@@ -468,45 +467,65 @@ export class ExtensionsScanner extends Disposable {
468467 }
469468
470469 async extractUserExtension ( extensionKey : ExtensionKey , zipPath : string , metadata : Metadata , removeIfExists : boolean , token : CancellationToken ) : Promise < ILocalExtension > {
471- await this . cleanUpGeneratedFoldersPromise . catch ( ( ) => undefined ) ;
472-
473470 const folderName = extensionKey . toString ( ) ;
474- const tempPath = path . join ( this . extensionsScannerService . userExtensionsLocation . fsPath , `.${ generateUuid ( ) } ` ) ;
475- const extensionPath = path . join ( this . extensionsScannerService . userExtensionsLocation . fsPath , folderName ) ;
471+ const tempLocation = URI . file ( path . join ( this . extensionsScannerService . userExtensionsLocation . fsPath , `.${ generateUuid ( ) } ` ) ) ;
472+ const extensionLocation = URI . file ( path . join ( this . extensionsScannerService . userExtensionsLocation . fsPath , folderName ) ) ;
476473
477- let exists = await this . fileService . exists ( URI . file ( extensionPath ) ) ;
474+ let exists = await this . fileService . exists ( extensionLocation ) ;
478475
479476 if ( exists && removeIfExists ) {
480477 try {
481- await pfs . Promises . rm ( extensionPath ) ;
478+ await this . deleteExtensionFromLocation ( extensionKey . id , extensionLocation , 'removeExisting' ) ;
482479 } catch ( error ) {
483- throw new ExtensionManagementError ( nls . localize ( 'errorDeleting' , "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again" , extensionPath , extensionKey . id ) , ExtensionManagementErrorCode . Delete ) ;
480+ throw new ExtensionManagementError ( nls . localize ( 'errorDeleting' , "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again" , extensionLocation . fsPath , extensionKey . id ) , ExtensionManagementErrorCode . Delete ) ;
484481 }
485482 exists = false ;
486483 }
487484
488485 if ( ! exists ) {
489- await this . extractAtLocation ( extensionKey , zipPath , tempPath , token ) ;
490- await this . extensionsScannerService . updateMetadata ( URI . file ( tempPath ) , metadata ) ;
491-
492486 try {
493- this . _onExtract . fire ( URI . file ( extensionPath ) ) ;
494- await this . rename ( extensionKey , tempPath , extensionPath , Date . now ( ) + ( 2 * 60 * 1000 ) /* Retry for 2 minutes */ ) ;
495- this . logService . info ( 'Renamed to' , extensionPath ) ;
496- } catch ( error ) {
487+ // Extract
497488 try {
498- await pfs . Promises . rm ( tempPath ) ;
499- } catch ( e ) { /* ignore */ }
500- if ( error . code === 'ENOTEMPTY' ) {
501- this . logService . info ( `Rename failed because extension was installed by another source. So ignoring renaming.` , extensionKey . id ) ;
502- } else {
503- this . logService . info ( `Rename failed because of ${ getErrorMessage ( error ) } . Deleted from extracted location` , tempPath ) ;
504- throw error ;
489+ this . logService . trace ( `Started extracting the extension from ${ zipPath } to ${ extensionLocation . fsPath } ` ) ;
490+ await extract ( zipPath , tempLocation . fsPath , { sourcePath : 'extension' , overwrite : true } , token ) ;
491+ this . logService . info ( `Extracted extension to ${ extensionLocation } :` , extensionKey . id ) ;
492+ } catch ( e ) {
493+ let errorCode = ExtensionManagementErrorCode . Extract ;
494+ if ( e instanceof ExtractError ) {
495+ if ( e . type === 'CorruptZip' ) {
496+ errorCode = ExtensionManagementErrorCode . CorruptZip ;
497+ } else if ( e . type === 'Incomplete' ) {
498+ errorCode = ExtensionManagementErrorCode . IncompleteZip ;
499+ }
500+ }
501+ throw new ExtensionManagementError ( e . message , errorCode ) ;
505502 }
503+
504+ await this . extensionsScannerService . updateMetadata ( tempLocation , metadata ) ;
505+
506+ // Rename
507+ try {
508+ this . logService . trace ( `Started renaming the extension from ${ tempLocation . fsPath } to ${ extensionLocation . fsPath } ` ) ;
509+ await this . rename ( extensionKey , tempLocation . fsPath , extensionLocation . fsPath , Date . now ( ) + ( 2 * 60 * 1000 ) /* Retry for 2 minutes */ ) ;
510+ this . logService . info ( 'Renamed to' , extensionLocation . fsPath ) ;
511+ } catch ( error ) {
512+ if ( error . code === 'ENOTEMPTY' ) {
513+ this . logService . info ( `Rename failed because extension was installed by another source. So ignoring renaming.` , extensionKey . id ) ;
514+ } else {
515+ this . logService . info ( `Rename failed because of ${ getErrorMessage ( error ) } . Deleted from extracted location` , tempLocation ) ;
516+ throw error ;
517+ }
518+ }
519+
520+ this . _onExtract . fire ( extensionLocation ) ;
521+
522+ } catch ( error ) {
523+ try { await this . fileService . del ( tempLocation , { recursive : true } ) ; } catch ( e ) { /* ignore */ }
524+ throw error ;
506525 }
507526 }
508527
509- return this . scanLocalExtension ( URI . file ( extensionPath ) , ExtensionType . User ) ;
528+ return this . scanLocalExtension ( extensionLocation , ExtensionType . User ) ;
510529 }
511530
512531 async scanMetadata ( local : ILocalExtension , profileLocation ?: URI ) : Promise < Metadata | undefined > {
@@ -544,12 +563,8 @@ export class ExtensionsScanner extends Disposable {
544563 await this . withUninstalledExtensions ( uninstalled => delete uninstalled [ extensionKey . toString ( ) ] ) ;
545564 }
546565
547- async removeExtension ( extension : ILocalExtension | IScannedExtension , type : string ) : Promise < void > {
548- this . logService . trace ( `Deleting ${ type } extension from disk` , extension . identifier . id , extension . location . fsPath ) ;
549- const renamedLocation = this . uriIdentityService . extUri . joinPath ( this . uriIdentityService . extUri . dirname ( extension . location ) , `._${ generateUuid ( ) } ` ) ;
550- await this . rename ( extension . identifier , extension . location . fsPath , renamedLocation . fsPath , Date . now ( ) + ( 2 * 60 * 1000 ) /* Retry for 2 minutes */ ) ;
551- await this . fileService . del ( renamedLocation , { recursive : true } ) ;
552- this . logService . info ( 'Deleted from disk' , extension . identifier . id , extension . location . fsPath ) ;
566+ removeExtension ( extension : ILocalExtension | IScannedExtension , type : string ) : Promise < void > {
567+ return this . deleteExtensionFromLocation ( extension . identifier . id , extension . location , type ) ;
553568 }
554569
555570 async removeUninstalledExtension ( extension : ILocalExtension | IScannedExtension ) : Promise < void > {
@@ -565,6 +580,12 @@ export class ExtensionsScanner extends Disposable {
565580 await this . extensionsProfileScannerService . addExtensionsToProfile ( extensions , toProfileLocation ) ;
566581 }
567582
583+ private async deleteExtensionFromLocation ( id : string , location : URI , type : string ) : Promise < void > {
584+ this . logService . trace ( `Deleting ${ type } extension from disk` , id , location . fsPath ) ;
585+ await this . fileService . del ( location , { recursive : true , atomic : { postfix : `.${ hash ( generateUuid ( ) ) . toString ( 16 ) } ${ DELETED_FOLDER_POSTFIX } ` } } ) ;
586+ this . logService . info ( `Deleted ${ type } extension from disk` , id , location . fsPath ) ;
587+ }
588+
568589 private async withUninstalledExtensions ( updateFn ?: ( uninstalled : IStringDictionary < boolean > ) => void ) : Promise < IStringDictionary < boolean > > {
569590 return this . uninstalledFileLimiter . queue ( async ( ) => {
570591 let raw : string | undefined ;
@@ -597,33 +618,6 @@ export class ExtensionsScanner extends Disposable {
597618 } ) ;
598619 }
599620
600- private async extractAtLocation ( identifier : IExtensionIdentifier , zipPath : string , location : string , token : CancellationToken ) : Promise < void > {
601- this . logService . trace ( `Started extracting the extension from ${ zipPath } to ${ location } ` ) ;
602-
603- // Clean the location
604- try {
605- await pfs . Promises . rm ( location ) ;
606- } catch ( e ) {
607- throw new ExtensionManagementError ( this . joinErrors ( e ) . message , ExtensionManagementErrorCode . Delete ) ;
608- }
609-
610- try {
611- await extract ( zipPath , location , { sourcePath : 'extension' , overwrite : true } , token ) ;
612- this . logService . info ( `Extracted extension to ${ location } :` , identifier . id ) ;
613- } catch ( e ) {
614- try { await pfs . Promises . rm ( location ) ; } catch ( e ) { /* Ignore */ }
615- let errorCode = ExtensionManagementErrorCode . Extract ;
616- if ( e instanceof ExtractError ) {
617- if ( e . type === 'CorruptZip' ) {
618- errorCode = ExtensionManagementErrorCode . CorruptZip ;
619- } else if ( e . type === 'Incomplete' ) {
620- errorCode = ExtensionManagementErrorCode . IncompleteZip ;
621- }
622- }
623- throw new ExtensionManagementError ( e . message , errorCode ) ;
624- }
625- }
626-
627621 private async rename ( identifier : IExtensionIdentifier , extractPath : string , renamePath : string , retryUntil : number ) : Promise < void > {
628622 try {
629623 await pfs . Promises . rename ( extractPath , renamePath ) ;
@@ -709,41 +703,39 @@ export class ExtensionsScanner extends Disposable {
709703 await Promise . allSettled ( toRemove . map ( e => this . removeUninstalledExtension ( e ) ) ) ;
710704 }
711705
712- private async removeGeneratedFolders ( ) : Promise < void > {
713- this . logService . trace ( 'ExtensionManagementService#removeGeneratedFolders ' ) ;
714- const promises : Promise < any > [ ] = [ ] ;
706+ private async removeTemporarilyDeletedFolders ( ) : Promise < void > {
707+ this . logService . trace ( 'ExtensionManagementService#removeTempDeleteFolders ' ) ;
708+
715709 let stat ;
716710 try {
717711 stat = await this . fileService . resolve ( this . extensionsScannerService . userExtensionsLocation ) ;
718712 } catch ( error ) {
719713 if ( toFileOperationResult ( error ) !== FileOperationResult . FILE_NOT_FOUND ) {
720714 this . logService . error ( error ) ;
721715 }
716+ return ;
722717 }
723- for ( const child of stat ?. children ?? [ ] ) {
724- if ( child . isDirectory && child . name . startsWith ( '._' ) && isUUID ( child . name . substring ( 2 ) ) ) {
725- promises . push ( ( async ( ) => {
726- this . logService . trace ( 'Deleting the generated extension folder' , child . resource . toString ( ) ) ;
727- try {
728- await this . fileService . del ( child . resource , { recursive : true } ) ;
729- this . logService . info ( 'Deleted the generated extension folder' , child . resource . toString ( ) ) ;
730- } catch ( error ) {
731- this . logService . error ( error ) ;
732- }
733- } ) ( ) ) ;
734- }
735- }
736- await Promise . allSettled ( promises ) ;
737- }
738718
739- private joinErrors ( errorOrErrors : ( Error | string ) | ( Array < Error | string > ) ) : Error {
740- const errors = Array . isArray ( errorOrErrors ) ? errorOrErrors : [ errorOrErrors ] ;
741- if ( errors . length === 1 ) {
742- return errors [ 0 ] instanceof Error ? < Error > errors [ 0 ] : new Error ( < string > errors [ 0 ] ) ;
719+ if ( ! stat ?. children ) {
720+ return ;
743721 }
744- return errors . reduce < Error > ( ( previousValue : Error , currentValue : Error | string ) => {
745- return new Error ( `${ previousValue . message } ${ previousValue . message ? ',' : '' } ${ currentValue instanceof Error ? currentValue . message : currentValue } ` ) ;
746- } , new Error ( '' ) ) ;
722+
723+ try {
724+ await Promise . allSettled ( stat . children . map ( async child => {
725+ if ( ! child . isDirectory || ! child . name . endsWith ( DELETED_FOLDER_POSTFIX ) ) {
726+ return ;
727+ }
728+ this . logService . trace ( 'Deleting the temporarily deleted folder' , child . resource . toString ( ) ) ;
729+ try {
730+ await this . fileService . del ( child . resource , { recursive : true } ) ;
731+ this . logService . trace ( 'Deleted the temporarily deleted folder' , child . resource . toString ( ) ) ;
732+ } catch ( error ) {
733+ if ( toFileOperationResult ( error ) !== FileOperationResult . FILE_NOT_FOUND ) {
734+ this . logService . error ( error ) ;
735+ }
736+ }
737+ } ) ) ;
738+ } catch ( error ) { /* ignore */ }
747739 }
748740
749741}
0 commit comments