From 86f47ff7437eb1686c674a0b2e271cd5cdce54c9 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 5 Mar 2026 14:35:05 +0000 Subject: [PATCH] fix(tsunpacker): use synchronous fs operations in tsunpacker to avoid readdir race conditions --- changelog.md | 8 +++++++ ts/00_commitinfo_data.ts | 2 +- ts/mod_unpack/classes.tsunpacker.ts | 36 +++++++++++------------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/changelog.md b/changelog.md index 58a05dc..a18d1f3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-05 - 4.1.17 - fix(tsunpacker) +use synchronous fs operations in tsunpacker to avoid readdir race conditions + +- Replaced async fs.promises.readdir/rename/rm/rmdir loops with fs.readdirSync/renameSync/rmSync/rmdirSync +- Removed readdir retry loops that attempted to handle partial/stale readdir results +- Updated comment to document rationale: avoid race conditions under signal pressure and XFS metadata lag +- Note: function remains async but now performs blocking sync filesystem calls which may block the event loop during unpack + ## 2026-03-05 - 4.1.16 - fix(mod_unpack) handle partial readdir results from signal-interrupted getdents64 when unpacking to ensure sibling removal and nested moves complete diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 40d192f..dfbe6ff 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsbuild', - version: '4.1.16', + version: '4.1.17', description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.' } diff --git a/ts/mod_unpack/classes.tsunpacker.ts b/ts/mod_unpack/classes.tsunpacker.ts index 20cc9a4..46799f9 100644 --- a/ts/mod_unpack/classes.tsunpacker.ts +++ b/ts/mod_unpack/classes.tsunpacker.ts @@ -90,9 +90,9 @@ export class TsUnpacker { * 2. Moving contents of the nested source folder up to the dest dir * 3. Removing the now-empty nested source folder * - * This approach never creates temporary sibling directories and never - * removes the destination directory itself, avoiding filesystem race - * conditions observed with the previous rename-rm-rename pattern. + * Uses synchronous fs operations to avoid race conditions with + * async readdir returning partial/stale results under signal pressure + * or XFS metadata lag (observed in process-group environments like gitzone). * * Returns true if unpacking was performed, false if skipped. */ @@ -108,32 +108,24 @@ export class TsUnpacker { const nestedPath = this.getNestedPath(); // Step 1: Remove sibling entries (everything in dest except the source folder) - // Loop handles partial readdir results from signal-interrupted getdents64 - let destEntries = await fs.promises.readdir(this.destDir); - while (destEntries.some(e => e !== this.sourceFolderName)) { - for (const entry of destEntries) { - if (entry !== this.sourceFolderName) { - await fs.promises.rm(path.join(this.destDir, entry), { recursive: true, force: true }); - } + const destEntries = fs.readdirSync(this.destDir); + for (const entry of destEntries) { + if (entry !== this.sourceFolderName) { + fs.rmSync(path.join(this.destDir, entry), { recursive: true, force: true }); } - destEntries = await fs.promises.readdir(this.destDir); } // Step 2: Move all contents from nested dir up to dest dir - // Loop handles partial readdir results from signal-interrupted getdents64 - let nestedEntries = await fs.promises.readdir(nestedPath); - while (nestedEntries.length > 0) { - for (const entry of nestedEntries) { - await fs.promises.rename( - path.join(nestedPath, entry), - path.join(this.destDir, entry), - ); - } - nestedEntries = await fs.promises.readdir(nestedPath); + const nestedEntries = fs.readdirSync(nestedPath); + for (const entry of nestedEntries) { + fs.renameSync( + path.join(nestedPath, entry), + path.join(this.destDir, entry), + ); } // Step 3: Remove the now-empty nested directory - await fs.promises.rmdir(nestedPath); + fs.rmdirSync(nestedPath); return true; }