Skip to content

Commit 65d8761

Browse files
committed
Fix race condition in mod asset extraction
1 parent 93b03a2 commit 65d8761

3 files changed

Lines changed: 94 additions & 68 deletions

File tree

lib/modloader/ModLoader.ts

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import type { ChildProcess } from 'node:child_process';
22
import { exec } from 'node:child_process';
3-
import { createWriteStream } from 'node:fs';
43
import * as fs from 'node:fs';
54
import * as Path from 'node:path';
6-
import { dirname, join, sep } from 'node:path';
5+
import { dirname, join } from 'node:path';
76
import { promisify } from 'node:util';
87
import { ncp } from 'ncp';
98
import fetch from 'node-fetch';
@@ -333,42 +332,68 @@ export class ModLoader {
333332
openZip(modFile, { lazyEntries: true, autoClose: true }, (e, f) => {
334333
if (e) {
335334
reject(e);
335+
} else {
336+
resolve(f);
336337
}
337-
resolve(f);
338338
});
339339
});
340340

341-
zipFile.readEntry();
342-
zipFile.on('error', e => process.stdout.write(String(e)));
343-
zipFile.on('entry', (entry: Entry) => {
344-
if (entry.fileName.endsWith('/')) {
345-
// Directory
346-
zipFile.readEntry();
347-
} else if (entry.fileName.startsWith('assets/') || entry.fileName.startsWith('data/')) {
348-
// File
349-
const prefix = entry.fileName.startsWith('assets/') ? 7 : 5;
350-
const targetFile = join(
351-
this.path,
352-
'mod_assets',
353-
entry.fileName.slice(prefix, entry.fileName.length),
354-
);
355-
const targetDir = dirname(targetFile);
356-
void this.ensureDirExists(targetDir).then(() => {
357-
zipFile.openReadStream(entry, (e, readStream) => {
358-
if (e) {
359-
throw e;
360-
}
361-
readStream.pipe(createWriteStream(targetFile));
362-
readStream.on('end', () => zipFile.readEntry());
363-
});
364-
}).catch((e: unknown) => {
365-
throw e;
366-
});
367-
} else {
368-
zipFile.readEntry();
369-
}
341+
await new Promise<void>((resolve, reject) => {
342+
let settled = false;
343+
const settle = (err?: Error): void => {
344+
// eslint-disable-next-line no-console,ts/restrict-template-expressions
345+
console.log(`Settling with error: ${err}`);
346+
if (settled) {
347+
return;
348+
}
349+
settled = true;
350+
if (err) {
351+
zipFile.close();
352+
reject(err);
353+
} else {
354+
resolve();
355+
}
356+
};
357+
358+
zipFile.on('error', (err: Error) => settle(err));
359+
zipFile.on('end', () => settle());
360+
zipFile.on('entry', (entry: Entry) => {
361+
// eslint-disable-next-line no-console
362+
console.log(`Processing entry: ${entry.fileName}`);
363+
if (entry.fileName.endsWith('/')) {
364+
// Directory
365+
zipFile.readEntry();
366+
} else if (entry.fileName.startsWith('assets/') || entry.fileName.startsWith('data/')) {
367+
// File
368+
const prefix = entry.fileName.startsWith('assets/') ? 7 : 5;
369+
const targetFile = join(
370+
this.path,
371+
'mod_assets',
372+
entry.fileName.slice(prefix, entry.fileName.length),
373+
);
374+
const targetDir = dirname(targetFile);
375+
fs.promises.mkdir(targetDir, { recursive: true }).then(() => {
376+
zipFile.openReadStream(entry, (e, readStream) => {
377+
if (e) {
378+
settle(e);
379+
return;
380+
}
381+
const chunks: Buffer[] = [];
382+
readStream.on('data', (chunk: Buffer) => chunks.push(chunk));
383+
readStream.on('error', (err: Error) => settle(err));
384+
readStream.on('end', () => {
385+
fs.promises.writeFile(targetFile, Buffer.concat(chunks))
386+
.then(() => zipFile.readEntry())
387+
.catch((err: Error) => settle(err));
388+
});
389+
});
390+
}).catch((err: Error) => settle(err));
391+
} else {
392+
zipFile.readEntry();
393+
}
394+
});
395+
zipFile.readEntry();
370396
});
371-
await new Promise(resolve => zipFile.on('end', resolve));
372397
}
373398

374399
/**
@@ -393,18 +418,6 @@ export class ModLoader {
393418
public async removeMods(): Promise<void> {
394419
await promisify(rimraf)(join(this.path, 'mods'));
395420
}
396-
397-
protected async ensureDirExists(path: string): Promise<void> {
398-
const segments = path.slice(this.path.length, this.path.length + path.length).split(sep);
399-
for (let i = 1; i <= segments.length; i++) {
400-
const subPath = join(this.path, segments.slice(0, i).join(sep));
401-
try {
402-
await fs.promises.stat(subPath);
403-
} catch {
404-
await fs.promises.mkdir(subPath);
405-
}
406-
}
407-
}
408421
}
409422

410423
export interface IModLoaderArgs {

package-lock.json

Lines changed: 35 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"pug": "^3.0.0",
8787
"rimraf": "^3.0.0",
8888
"xml2js": "^0.4.19",
89-
"yauzl": "^2.10.0"
89+
"yauzl": "^3.3.1"
9090
},
9191
"devDependencies": {
9292
"@rubensworks/eslint-config": "^3.1.0",

0 commit comments

Comments
 (0)