diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 943660be..01f0d772 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -170,6 +170,9 @@ async function downloadAndExtractTar(url: string, targetDir: string) { } } +// Factorio downloads in progress by target directory, shared by every server in the process +const downloadsInProgress = new Map>(); + /** * Find factorio data directory of the given version * @@ -1001,8 +1004,10 @@ export class FactorioServer extends events.EventEmitter { return; } + // A version that is still being extracted can already look installed + const dst = path.join(this._factorioDir, latestVersion.version); const installedVersions = await listFactorioVersions(this._factorioDir); - if (!installedVersions.versions.has(latestVersion.version)) { + if (!installedVersions.versions.has(latestVersion.version) || downloadsInProgress.has(dst)) { const v = latestVersion.version; // Can't download if not linux or this is a direct install if (installedVersions.direct) { @@ -1022,10 +1027,18 @@ export class FactorioServer extends events.EventEmitter { return; } - // Download the newer version - this._logger.info(`A newer version of factorio is available (${v}), starting download...`); - const dst = path.join(this._factorioDir, latestVersion.version); - await downloadAndExtractTar(latestVersion.headlessUrl, dst); + // Download the newer version, or wait for another server already downloading it + let download = downloadsInProgress.get(dst); + if (download) { + this._logger.info(`A newer version of factorio is available (${v}), waiting for download...`); + } else { + this._logger.info(`A newer version of factorio is available (${v}), starting download...`); + download = downloadAndExtractTar(latestVersion.headlessUrl, dst).finally(() => { + downloadsInProgress.delete(dst); + }); + downloadsInProgress.set(dst, download); + } + await download; this._logger.info(`Downloaded factorio version ${v}`); } } diff --git a/test/host/server.js b/test/host/server.js index c8965ed6..1b14e200 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -505,6 +505,62 @@ describe("host/server", function() { }); /* eslint-enable no-loop-func */ } + describe("concurrent downloads", function() { + const versions = [{ stable: true, version: "0.1.5", headlessUrl: "test1" }]; + + // Holds the mocked download open until release() and resolves waiting once a server waits for it + function holdDownload(factorioDir) { + const held = { fetches: 0 }; + const release = new Promise(resolve => { held.release = resolve; }); + held.fetching = new Promise(resolve => { + global.fetch = async function() { + held.fetches += 1; + resolve(); + await release; + return { ok: false, status: -1, statusText: "Fetch called" }; + }; + }); + let waited; + held.waiting = new Promise(resolve => { waited = resolve; }); + const logger = { info: line => line.endsWith("waiting for download...") && waited() }; + held.servers = [0, 1].map(() => new hostServer.FactorioServer(factorioDir, writePath, { logger })); + Object.defineProperty(process, "platform", { value: "linux" }); + return held; + } + + it("should download once when servers check at the same time", async function() { + const held = holdDownload(path.join("test", "file", "factorio")); + let settled = 0; + const checks = held.servers.map(s => s.checkForUpdates(versions).finally(() => { settled += 1; })); + + await held.waiting; + assert.equal(held.fetches, 1); + assert.equal(settled, 0); + + held.release(); + await Promise.all(checks.map(check => assert.rejects(check, /Fetch called/))); + assert.equal(held.fetches, 1); + }); + it("should wait for a download in progress whose version already looks installed", async function() { + const factorioDir = path.join("temp", "test", "download-lock"); + await fs.rm(factorioDir, { force: true, recursive: true }); + await fs.mkdir(factorioDir, { recursive: true }); + const held = holdDownload(factorioDir); + const first = held.servers[0].checkForUpdates(versions); + await held.fetching; + + // The extraction has written the changelog but has not finished + const dataDir = path.join(factorioDir, "0.1.5", "data"); + await fs.mkdir(dataDir, { recursive: true }); + await fs.writeFile(path.join(dataDir, "changelog.txt"), "Version: 0.1.5\n"); + const second = held.servers[1].checkForUpdates(versions); + + await held.waiting; + held.release(); + await Promise.all([first, second].map(check => assert.rejects(check, /Fetch called/))); + assert.equal(held.fetches, 1); + }); + }); it("should download a version correctly (live api)", async function() { slowTest(this); externalTest(this);