From 48bf63f9285601d21c6cf4d8527a7cbc007a2db8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:13:34 +0000 Subject: [PATCH 1/2] Initial plan From 83aade06c8348d2708145bdef6ee977f1fb89c6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:20:05 +0000 Subject: [PATCH 2/2] Fix race condition in FileSystem.create*Link helpers by factoring isExistError handler into dedicated helpers Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com> --- ...dition-in-filesystem_2026-02-20-23-14.json | 10 ++ libraries/node-core-library/src/FileSystem.ts | 92 +++++++++++++------ 2 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 common/changes/@rushstack/node-core-library/fix-race-condition-in-filesystem_2026-02-20-23-14.json diff --git a/common/changes/@rushstack/node-core-library/fix-race-condition-in-filesystem_2026-02-20-23-14.json b/common/changes/@rushstack/node-core-library/fix-race-condition-in-filesystem_2026-02-20-23-14.json new file mode 100644 index 00000000000..80e0051ae10 --- /dev/null +++ b/common/changes/@rushstack/node-core-library/fix-race-condition-in-filesystem_2026-02-20-23-14.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Fix race condition in FileSystem.create*Link helpers: EEXIST errors that occur after ensureFolder/ensureFolderAsync are now handled consistently with the initial EEXIST handling.", + "type": "patch" + } + ], + "packageName": "@rushstack/node-core-library" +} diff --git a/libraries/node-core-library/src/FileSystem.ts b/libraries/node-core-library/src/FileSystem.ts index da107dd1604..a2ff0b74d2f 100644 --- a/libraries/node-core-library/src/FileSystem.ts +++ b/libraries/node-core-library/src/FileSystem.ts @@ -1518,25 +1518,33 @@ export class FileSystem { ); } + private static _handleLinkExistError( + linkFn: () => void, + options: IInternalFileSystemCreateLinkOptions, + error: Error + ): void { + switch (options.alreadyExistsBehavior) { + case AlreadyExistsBehavior.Ignore: + break; + case AlreadyExistsBehavior.Overwrite: + // fsx.linkSync does not allow overwriting so we must manually delete. If it's + // a folder, it will throw an error. + this.deleteFile(options.newLinkPath); + linkFn(); + break; + case AlreadyExistsBehavior.Error: + default: + throw error; + } + } + private static _handleLink(linkFn: () => void, options: IInternalFileSystemCreateLinkOptions): void { try { linkFn(); } catch (error) { if (FileSystem.isExistError(error as Error)) { // Link exists, handle it - switch (options.alreadyExistsBehavior) { - case AlreadyExistsBehavior.Ignore: - break; - case AlreadyExistsBehavior.Overwrite: - // fsx.linkSync does not allow overwriting so we must manually delete. If it's - // a folder, it will throw an error. - this.deleteFile(options.newLinkPath); - linkFn(); - break; - case AlreadyExistsBehavior.Error: - default: - throw error; - } + FileSystem._handleLinkExistError(linkFn, options, error as Error); } else { // When attempting to create a link in a directory that does not exist, an ENOENT // or ENOTDIR error is thrown, so we should ensure the directory exists before @@ -1547,7 +1555,17 @@ export class FileSystem { (!options.linkTargetMustExist || FileSystem.exists(options.linkTargetPath)) ) { this.ensureFolder(nodeJsPath.dirname(options.newLinkPath)); - linkFn(); + try { + linkFn(); + } catch (retryError) { + if (FileSystem.isExistError(retryError as Error)) { + // Another concurrent process may have created the link between the ensureFolder + // call and the retry; handle it the same way as the initial exist error. + FileSystem._handleLinkExistError(linkFn, options, retryError as Error); + } else { + throw retryError; + } + } } else { throw error; } @@ -1555,6 +1573,26 @@ export class FileSystem { } } + private static async _handleLinkExistErrorAsync( + linkFn: () => Promise, + options: IInternalFileSystemCreateLinkOptions, + error: Error + ): Promise { + switch (options.alreadyExistsBehavior) { + case AlreadyExistsBehavior.Ignore: + break; + case AlreadyExistsBehavior.Overwrite: + // fsx.linkSync does not allow overwriting so we must manually delete. If it's + // a folder, it will throw an error. + await this.deleteFileAsync(options.newLinkPath); + await linkFn(); + break; + case AlreadyExistsBehavior.Error: + default: + throw error; + } + } + private static async _handleLinkAsync( linkFn: () => Promise, options: IInternalFileSystemCreateLinkOptions @@ -1564,19 +1602,7 @@ export class FileSystem { } catch (error) { if (FileSystem.isExistError(error as Error)) { // Link exists, handle it - switch (options.alreadyExistsBehavior) { - case AlreadyExistsBehavior.Ignore: - break; - case AlreadyExistsBehavior.Overwrite: - // fsx.linkSync does not allow overwriting so we must manually delete. If it's - // a folder, it will throw an error. - await this.deleteFileAsync(options.newLinkPath); - await linkFn(); - break; - case AlreadyExistsBehavior.Error: - default: - throw error; - } + await FileSystem._handleLinkExistErrorAsync(linkFn, options, error as Error); } else { // When attempting to create a link in a directory that does not exist, an ENOENT // or ENOTDIR error is thrown, so we should ensure the directory exists before @@ -1587,7 +1613,17 @@ export class FileSystem { (!options.linkTargetMustExist || (await FileSystem.existsAsync(options.linkTargetPath))) ) { await this.ensureFolderAsync(nodeJsPath.dirname(options.newLinkPath)); - await linkFn(); + try { + await linkFn(); + } catch (retryError) { + if (FileSystem.isExistError(retryError as Error)) { + // Another concurrent process may have created the link between the ensureFolderAsync + // call and the retry; handle it the same way as the initial exist error. + await FileSystem._handleLinkExistErrorAsync(linkFn, options, retryError as Error); + } else { + throw retryError; + } + } } else { throw error; }