-
Notifications
You must be signed in to change notification settings - Fork 698
fix: [node-core-library] Retry transient Windows EPERM/EBUSY in FileSystem.deleteFolder(Async) #5836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: [node-core-library] Retry transient Windows EPERM/EBUSY in FileSystem.deleteFolder(Async) #5836
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@rushstack/node-core-library", | ||
| "comment": "Make FileSystem.deleteFolder, FileSystem.deleteFolderAsync, FileSystem.ensureEmptyFolder, and FileSystem.ensureEmptyFolderAsync resilient to transient Windows EPERM/EBUSY/ENOTEMPTY errors by using native fs.rm with retries.", | ||
| "type": "patch" | ||
| } | ||
| ], | ||
| "packageName": "@rushstack/node-core-library" | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -722,14 +722,14 @@ export class FileSystem { | |||||
|
|
||||||
| /** | ||||||
| * Deletes a folder, including all of its contents. | ||||||
| * Behind the scenes is uses `fs-extra.removeSync()`. | ||||||
| * Behind the scenes it uses `fs.rmSync()`. | ||||||
| * @remarks | ||||||
| * Does not throw if the folderPath does not exist. | ||||||
| * @param folderPath - The absolute or relative path to the folder which should be deleted. | ||||||
| */ | ||||||
| public static deleteFolder(folderPath: string): void { | ||||||
| FileSystem._wrapException(() => { | ||||||
| fsx.removeSync(folderPath); | ||||||
| fs.rmSync(folderPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stick this options object in a shared constant? |
||||||
| }); | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -738,30 +738,60 @@ export class FileSystem { | |||||
| */ | ||||||
| public static async deleteFolderAsync(folderPath: string): Promise<void> { | ||||||
| await FileSystem._wrapExceptionAsync(() => { | ||||||
| return fsx.remove(folderPath); | ||||||
| return fsPromises.rm(folderPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Deletes the content of a folder, but not the folder itself. Also ensures the folder exists. | ||||||
| * Behind the scenes it uses `fs-extra.emptyDirSync()`. | ||||||
| * Each child entry is removed via `fs.rmSync()`. | ||||||
| * @remarks | ||||||
| * This is a workaround for a common race condition, where the virus scanner holds a lock on the folder | ||||||
| * for a brief period after it was deleted, causing EBUSY errors for any code that tries to recreate the folder. | ||||||
| * @param folderPath - The absolute or relative path to the folder which should have its contents deleted. | ||||||
| */ | ||||||
| public static ensureEmptyFolder(folderPath: string): void { | ||||||
| FileSystem._wrapException(() => { | ||||||
| fsx.emptyDirSync(folderPath); | ||||||
| let items: string[]; | ||||||
| try { | ||||||
| items = fsx.readdirSync(folderPath); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||||||
| } catch { | ||||||
| fsx.ensureDirSync(folderPath); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifically check for a not-exist error using |
||||||
| return; | ||||||
| } | ||||||
| for (const item of items) { | ||||||
| fs.rmSync(nodeJsPath.join(folderPath, item), { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forward shashes work fine on Windows.
Suggested change
|
||||||
| recursive: true, | ||||||
| force: true, | ||||||
| maxRetries: 3, | ||||||
| retryDelay: 100 | ||||||
| }); | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * An async version of {@link FileSystem.ensureEmptyFolder}. | ||||||
| */ | ||||||
| public static async ensureEmptyFolderAsync(folderPath: string): Promise<void> { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same feedback as the sync version applies here. |
||||||
| await FileSystem._wrapExceptionAsync(() => { | ||||||
| return fsx.emptyDir(folderPath); | ||||||
| await FileSystem._wrapExceptionAsync(async () => { | ||||||
| let items: string[]; | ||||||
| try { | ||||||
| items = await fsx.readdir(folderPath); | ||||||
| } catch { | ||||||
| await fsx.ensureDir(folderPath); | ||||||
| return; | ||||||
| } | ||||||
| await Promise.all( | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Limit concurrency here using |
||||||
| items.map((item) => | ||||||
| fsPromises.rm(nodeJsPath.join(folderPath, item), { | ||||||
| recursive: true, | ||||||
| force: true, | ||||||
| maxRetries: 3, | ||||||
| retryDelay: 100 | ||||||
| }) | ||||||
| ) | ||||||
| ); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.