Skip to content

Commit e195ed8

Browse files
authored
adopt atomic delete for extensions (microsoft#187054)
microsoft#180695 adopt atomic delete for extensions
1 parent 48531d4 commit e195ed8

File tree

1 file changed

+78
-86
lines changed

1 file changed

+78
-86
lines changed

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

Lines changed: 78 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
1010
import { toErrorMessage } from 'vs/base/common/errorMessage';
1111
import { getErrorMessage } from 'vs/base/common/errors';
1212
import { Emitter } from 'vs/base/common/event';
13+
import { hash } from 'vs/base/common/hash';
1314
import { Disposable } from 'vs/base/common/lifecycle';
1415
import { ResourceSet } from 'vs/base/common/map';
1516
import { Schemas } from 'vs/base/common/network';
@@ -19,7 +20,7 @@ import { joinPath } from 'vs/base/common/resources';
1920
import * as semver from 'vs/base/common/semver/semver';
2021
import { isBoolean, isUndefined } from 'vs/base/common/types';
2122
import { URI } from 'vs/base/common/uri';
22-
import { generateUuid, isUUID } from 'vs/base/common/uuid';
23+
import { generateUuid } from 'vs/base/common/uuid';
2324
import * as pfs from 'vs/base/node/pfs';
2425
import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip';
2526
import * 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+
6568
export 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

Comments
 (0)