From 7a1c2d82b96bc5632096e66122f876548d2f080b Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 5 Mar 2026 15:09:53 +0000 Subject: [PATCH] fix(mod_unpack): iterate directories with opendirSync/readSync to avoid missing entries on XFS and ensure directory handles are closed --- changelog.md | 8 +++++++ ts/00_commitinfo_data.ts | 2 +- ts/mod_unpack/classes.tsunpacker.ts | 34 ++++++++++++++++------------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 7fe4414..f350dbb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-05 - 4.1.24 - fix(mod_unpack) +iterate directories with opendirSync/readSync to avoid missing entries on XFS and ensure directory handles are closed + +- Replaced readdirSync loops with opendirSync + readSync for destination and nested directories to provide a single stable directory handle during iteration +- Added explicit closeSync() calls to close directory handles and avoid resource leaks +- Avoids partial results/missed entries that can occur when repeatedly calling readdirSync (observed on XFS with delayed metadata) +- Preserves existing renameSync move logic and increments moved counter while cleaning up the now-empty nested directory + ## 2026-03-05 - 4.1.23 - fix(mod_unpack) handle partial readdirSync results when moving nested directory entries and add diagnostic log diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index ea019a4..2ae2b27 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.23', + version: '4.1.24', 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 41d207f..1e0c954 100644 --- a/ts/mod_unpack/classes.tsunpacker.ts +++ b/ts/mod_unpack/classes.tsunpacker.ts @@ -108,27 +108,31 @@ export class TsUnpacker { const nestedPath = this.getNestedPath(); // Step 1: Remove sibling entries (everything in dest except the source folder) - 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 }); + // Use opendirSync to keep a single directory handle open for reliable iteration + const destDir = fs.opendirSync(this.destDir); + let destEntry; + while ((destEntry = destDir.readSync()) !== null) { + if (destEntry.name !== this.sourceFolderName) { + fs.rmSync(path.join(this.destDir, destEntry.name), { recursive: true, force: true }); } } + destDir.closeSync(); // Step 2: Move all contents from nested dir up to dest dir - // Use a loop to handle any partial readdirSync results + // Use opendirSync to keep a single directory handle open — this avoids + // partial results from readdirSync which opens a fresh file descriptor + // each call and can miss entries on XFS with delayed metadata logging + const nestedDir = fs.opendirSync(nestedPath); + let nestedEntry; let moved = 0; - while (true) { - const entries = fs.readdirSync(nestedPath); - if (entries.length === 0) break; - for (const entry of entries) { - fs.renameSync( - path.join(nestedPath, entry), - path.join(this.destDir, entry), - ); - moved++; - } + while ((nestedEntry = nestedDir.readSync()) !== null) { + fs.renameSync( + path.join(nestedPath, nestedEntry.name), + path.join(this.destDir, nestedEntry.name), + ); + moved++; } + nestedDir.closeSync(); // Step 3: Remove the now-empty nested directory fs.rmdirSync(nestedPath);