-
Notifications
You must be signed in to change notification settings - Fork 29
Description
vropkg: EMFILE "too many files open" on Windows due to un-awaited archiver streams (descriptor leak & uncontrolled concurrency)
Summary
Running vropkg on Windows for packages with many elements fails with:
Error: EMFILE: too many open files
Archive write streams are created and finalized without awaiting their completion. High parallelism (Promise.all across elements) causes many simultaneous open file descriptors until the OS limit is hit (Windows defaults are low). Also introduces a race where signing may read files before archives are flushed.
Affected Code
typescript/vropkg/src/serialize/flat.tsinitializeContext.bundlereturns the result of.finalize()(void) but is typed/used like aPromise<void>.serializeFlatElementslaunches many async tasks per element concurrently.
typescript/vropkg/src/serialize/util.tszipbundlestarts anarchiverpipeline and returns immediately; no Promise on streamclose.
typescript/vropkg/src/packaging.tsarchive()creates and pipes anarchiverbut callers don't wait for the destination stream to finish.
Impact
- Frequent failure on Windows with medium/large packages (e.g. ≥512 elements).
- Potential race: signatures may be generated before archive contents are fully written.
Environment (example)
- OS: Windows 10 / Windows Server 2019
- Node.js: 18.x / 20.x / 22.x
- Branch:
main(as of 2025-08-27)
Steps to Reproduce
- Prepare a project producing a vRO package with 100+ elements (some with bundles/resources).
- Run
vropkgto build the package. - Observe eventual failure with
EMFILE.
Expected
Packaging completes; file descriptors are reused; signatures generated after all archive writes complete.
Actual
Multiple archives in progress simultaneously; write streams not awaited; EMFILE thrown.
Root Cause
archiver.finalize() is not asynchronous (returns void). Code assumes it can be awaited. Without listening to finish/close on the output stream, promises resolve too early (or are not real promises), allowing unbounded concurrency and exhausting file handles.
Proposed Fix
- Wrap each archiving operation in a real
Promiseresolving on output streamclose(orfinish) and rejecting onerror. - Correct return types (ensure functions actually return
Promise<void>). - Await archive promises (e.g.,
await context.save(...)). - Add simple concurrency control for element serialization (e.g.,
p-limitwith a reasonable cap or process elements sequentially). - Ensure signature generation runs only after all archive writes have completed.
- Add an integration test packaging a large synthetic set of elements to assert success without
EMFILE.
Patch Sketch (conceptual)
// In serialize/util.ts
export const zipbundle = (target: string) => {
fs.mkdirsSync(target);
return (file: string) => (sourcePath: string, isDir: boolean): Promise<void> => {
const absoluteZipPath = path.join(target, file);
if (!isDir) {
fs.copySync(sourcePath, absoluteZipPath);
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(absoluteZipPath);
const archive = archiver('zip', { zlib: { level: 9 }});
output.on('close', resolve).on('error', reject);
archive.on('error', reject);
archive.directory(sourcePath, false);
archive.pipe(output);
archive.finalize();
});
};
};(Apply similar pattern for final package bundle and resource element bundles.)
Workarounds
- Reduce elements per run (split package).
- Increase Windows file handle limit (not always feasible).
- Run on Linux/macOS (higher default limits) — mitigates but doesn’t fix root cause.
Acceptance Criteria
- Large package builds on Windows without
EMFILE. - Signatures are created only after archive completion.
- No regression in output structure.
Additional Notes
Fix is low risk and localized to I/O lifecycle management; may slightly reduce parallel throughput but improves stability and determinism.
Attached Log:
[INFO] info: Using certificate file target/keystore-1.0.4/private_key.pem
[INFO] info: Using certificate file target/keystore-1.0.4/cert.pem
[ERROR] node:fs:562
[ERROR] return binding.open(
[ERROR] ^
[ERROR]
[ERROR] Error: EMFILE: too many open files, open 'C:\Users\txdcy\Documents\projects\di\workflows\target\vropkg\elements\758fd262-5974-497d-9a6a-4a4223697fc4\content-signature'
[ERROR] at Object.openSync (node:fs:562:18)
[ERROR] at Object.writeFileSync (node:fs:2440:35)
[ERROR] at C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:39:12
[ERROR] at Generator.next (<anonymous>)
[ERROR] at C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:8:71
[ERROR] at new Promise (<anonymous>)
[ERROR] at __awaiter (C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:4:12)
[ERROR] at Object.contentSignature (C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:35:51)
[ERROR] at C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\flat.js:220:20
[ERROR] at Generator.next (<anonymous>)
[ERROR] at fulfilled (C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\flat.js:5:58) {
[ERROR] errno: -4066,
[ERROR] code: 'EMFILE',
[ERROR] syscall: 'open',
[ERROR] path: 'C:\\Users\\txdcy\\Documents\\projects\\di\\workflows\\target\\vropkg\\elements\\758fd262-5974-497d-9a6a-4a4223697fc4\\content-signature'
[ERROR] }
[ERROR]
[ERROR] Node.js v22.16.0
[INFO] Running vropkg... finished