Skip to content

Commit f614374

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

1 file changed

Lines changed: 56 additions & 45 deletions

File tree

lib/modloader/ModLoader.ts

Lines changed: 56 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,66 @@ 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+
console.log(`Settling with error: ${err}`);
345+
if (settled) {
346+
return;
347+
}
348+
settled = true;
349+
if (err) {
350+
zipFile.close();
351+
reject(err);
352+
} else {
353+
resolve();
354+
}
355+
};
356+
357+
zipFile.on('error', (err: Error) => settle(err));
358+
zipFile.on('end', () => settle());
359+
zipFile.on('entry', (entry: Entry) => {
360+
console.log(`Processing entry: ${entry.fileName}`);
361+
if (entry.fileName.endsWith('/')) {
362+
// Directory
363+
zipFile.readEntry();
364+
} else if (entry.fileName.startsWith('assets/') || entry.fileName.startsWith('data/')) {
365+
// File
366+
const prefix = entry.fileName.startsWith('assets/') ? 7 : 5;
367+
const targetFile = join(
368+
this.path,
369+
'mod_assets',
370+
entry.fileName.slice(prefix, entry.fileName.length),
371+
);
372+
const targetDir = dirname(targetFile);
373+
fs.promises.mkdir(targetDir, { recursive: true }).then(() => {
374+
zipFile.openReadStream(entry, (e, readStream) => {
375+
if (e) {
376+
settle(e);
377+
return;
378+
}
379+
const chunks: Buffer[] = [];
380+
readStream.on('data', (chunk: Buffer) => chunks.push(chunk));
381+
readStream.on('error', (err: Error) => settle(err));
382+
readStream.on('end', () => {
383+
fs.promises.writeFile(targetFile, Buffer.concat(chunks))
384+
.then(() => zipFile.readEntry())
385+
.catch((err: Error) => settle(err));
386+
});
387+
});
388+
}).catch((err: Error) => settle(err));
389+
} else {
390+
zipFile.readEntry();
391+
}
392+
});
393+
zipFile.readEntry();
370394
});
371-
await new Promise(resolve => zipFile.on('end', resolve));
372395
}
373396

374397
/**
@@ -393,18 +416,6 @@ export class ModLoader {
393416
public async removeMods(): Promise<void> {
394417
await promisify(rimraf)(join(this.path, 'mods'));
395418
}
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-
}
408419
}
409420

410421
export interface IModLoaderArgs {

0 commit comments

Comments
 (0)