@@ -10,6 +10,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
10
10
import { toErrorMessage } from 'vs/base/common/errorMessage' ;
11
11
import { getErrorMessage } from 'vs/base/common/errors' ;
12
12
import { Emitter } from 'vs/base/common/event' ;
13
+ import { hash } from 'vs/base/common/hash' ;
13
14
import { Disposable } from 'vs/base/common/lifecycle' ;
14
15
import { ResourceSet } from 'vs/base/common/map' ;
15
16
import { Schemas } from 'vs/base/common/network' ;
@@ -19,7 +20,7 @@ import { joinPath } from 'vs/base/common/resources';
19
20
import * as semver from 'vs/base/common/semver/semver' ;
20
21
import { isBoolean , isUndefined } from 'vs/base/common/types' ;
21
22
import { URI } from 'vs/base/common/uri' ;
22
- import { generateUuid , isUUID } from 'vs/base/common/uuid' ;
23
+ import { generateUuid } from 'vs/base/common/uuid' ;
23
24
import * as pfs from 'vs/base/node/pfs' ;
24
25
import { extract , ExtractError , IFile , zip } from 'vs/base/node/zip' ;
25
26
import * as nls from 'vs/nls' ;
@@ -62,6 +63,8 @@ export interface INativeServerExtensionManagementService extends IExtensionManag
62
63
markAsUninstalled ( ...extensions : IExtension [ ] ) : Promise < void > ;
63
64
}
64
65
66
+ const DELETED_FOLDER_POSTFIX = '.vsctmp' ;
67
+
65
68
export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService {
66
69
67
70
private readonly extensionsScanner : ExtensionsScanner ;
@@ -417,14 +420,11 @@ export class ExtensionsScanner extends Disposable {
417
420
private readonly _onExtract = this . _register ( new Emitter < URI > ( ) ) ;
418
421
readonly onExtract = this . _onExtract . event ;
419
422
420
- private cleanUpGeneratedFoldersPromise : Promise < void > = Promise . resolve ( ) ;
421
-
422
423
constructor (
423
424
private readonly beforeRemovingExtension : ( e : ILocalExtension ) => Promise < void > ,
424
425
@IFileService private readonly fileService : IFileService ,
425
426
@IExtensionsScannerService private readonly extensionsScannerService : IExtensionsScannerService ,
426
427
@IExtensionsProfileScannerService private readonly extensionsProfileScannerService : IExtensionsProfileScannerService ,
427
- @IUriIdentityService private readonly uriIdentityService : IUriIdentityService ,
428
428
@ILogService private readonly logService : ILogService ,
429
429
) {
430
430
super ( ) ;
@@ -433,9 +433,8 @@ export class ExtensionsScanner extends Disposable {
433
433
}
434
434
435
435
async cleanUp ( ) : Promise < void > {
436
+ await this . removeTemporarilyDeletedFolders ( ) ;
436
437
await this . removeUninstalledExtensions ( ) ;
437
- this . cleanUpGeneratedFoldersPromise = this . cleanUpGeneratedFoldersPromise . then ( ( ) => this . removeGeneratedFolders ( ) ) ;
438
- await this . cleanUpGeneratedFoldersPromise ;
439
438
}
440
439
441
440
async scanExtensions ( type : ExtensionType | null , profileLocation : URI ) : Promise < ILocalExtension [ ] > {
@@ -468,45 +467,65 @@ export class ExtensionsScanner extends Disposable {
468
467
}
469
468
470
469
async extractUserExtension ( extensionKey : ExtensionKey , zipPath : string , metadata : Metadata , removeIfExists : boolean , token : CancellationToken ) : Promise < ILocalExtension > {
471
- await this . cleanUpGeneratedFoldersPromise . catch ( ( ) => undefined ) ;
472
-
473
470
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 ) ) ;
476
473
477
- let exists = await this . fileService . exists ( URI . file ( extensionPath ) ) ;
474
+ let exists = await this . fileService . exists ( extensionLocation ) ;
478
475
479
476
if ( exists && removeIfExists ) {
480
477
try {
481
- await pfs . Promises . rm ( extensionPath ) ;
478
+ await this . deleteExtensionFromLocation ( extensionKey . id , extensionLocation , 'removeExisting' ) ;
482
479
} 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 ) ;
484
481
}
485
482
exists = false ;
486
483
}
487
484
488
485
if ( ! exists ) {
489
- await this . extractAtLocation ( extensionKey , zipPath , tempPath , token ) ;
490
- await this . extensionsScannerService . updateMetadata ( URI . file ( tempPath ) , metadata ) ;
491
-
492
486
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
497
488
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 ) ;
505
502
}
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 ;
506
525
}
507
526
}
508
527
509
- return this . scanLocalExtension ( URI . file ( extensionPath ) , ExtensionType . User ) ;
528
+ return this . scanLocalExtension ( extensionLocation , ExtensionType . User ) ;
510
529
}
511
530
512
531
async scanMetadata ( local : ILocalExtension , profileLocation ?: URI ) : Promise < Metadata | undefined > {
@@ -544,12 +563,8 @@ export class ExtensionsScanner extends Disposable {
544
563
await this . withUninstalledExtensions ( uninstalled => delete uninstalled [ extensionKey . toString ( ) ] ) ;
545
564
}
546
565
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 ) ;
553
568
}
554
569
555
570
async removeUninstalledExtension ( extension : ILocalExtension | IScannedExtension ) : Promise < void > {
@@ -565,6 +580,12 @@ export class ExtensionsScanner extends Disposable {
565
580
await this . extensionsProfileScannerService . addExtensionsToProfile ( extensions , toProfileLocation ) ;
566
581
}
567
582
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
+
568
589
private async withUninstalledExtensions ( updateFn ?: ( uninstalled : IStringDictionary < boolean > ) => void ) : Promise < IStringDictionary < boolean > > {
569
590
return this . uninstalledFileLimiter . queue ( async ( ) => {
570
591
let raw : string | undefined ;
@@ -597,33 +618,6 @@ export class ExtensionsScanner extends Disposable {
597
618
} ) ;
598
619
}
599
620
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
-
627
621
private async rename ( identifier : IExtensionIdentifier , extractPath : string , renamePath : string , retryUntil : number ) : Promise < void > {
628
622
try {
629
623
await pfs . Promises . rename ( extractPath , renamePath ) ;
@@ -709,41 +703,39 @@ export class ExtensionsScanner extends Disposable {
709
703
await Promise . allSettled ( toRemove . map ( e => this . removeUninstalledExtension ( e ) ) ) ;
710
704
}
711
705
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
+
715
709
let stat ;
716
710
try {
717
711
stat = await this . fileService . resolve ( this . extensionsScannerService . userExtensionsLocation ) ;
718
712
} catch ( error ) {
719
713
if ( toFileOperationResult ( error ) !== FileOperationResult . FILE_NOT_FOUND ) {
720
714
this . logService . error ( error ) ;
721
715
}
716
+ return ;
722
717
}
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
- }
738
718
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 ;
743
721
}
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 */ }
747
739
}
748
740
749
741
}
0 commit comments