diff --git a/electron/index.js b/electron/index.js index d078b649..14a19a64 100644 --- a/electron/index.js +++ b/electron/index.js @@ -3,10 +3,11 @@ const { app, BrowserWindow, Menu, MenuItem, session } = require("electron"); const path = require("path"); const url = require("url"); -const childProcess = require("child_process"); const { ipcMain, shell } = require("electron"); const fs = require("fs"); -const steam = require('./steam'); +const steam = require("./steam"); +const asyncLock = require("async-lock"); + const isDev = process.argv.indexOf("--dev") >= 0; const isLocal = process.argv.indexOf("--local") >= 0; @@ -153,7 +154,82 @@ ipcMain.on("exit-app", (event, flag) => { app.quit(); }); -function performFsJob(job) { +let renameCounter = 1; + +const fileLock = new asyncLock({ + timeout: 30000, + maxPending: 1000, +}); + +function niceFileName(filename) { + return filename.replace(storePath, "@"); +} + +async function writeFileSafe(filename, contents) { + ++renameCounter; + const prefix = "[ " + renameCounter + ":" + niceFileName(filename) + " ] "; + const transactionId = String(new Date().getTime()) + "." + renameCounter; + + if (fileLock.isBusy()) { + console.warn(prefix, "Concurrent write process on", filename); + } + + fileLock.acquire(filename, async () => { + console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId); + + if (!fs.existsSync(filename)) { + // this one is easy + console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename)); + await fs.promises.writeFile(filename, contents, { encoding: "utf8" }); + return; + } + + // first, write a temporary file (.tmp-XXX) + const tempName = filename + ".tmp-" + transactionId; + console.log(prefix, "Writing temporary file", niceFileName(tempName)); + await fs.promises.writeFile(tempName, contents, { encoding: "utf8" }); + + // now, rename the original file to (.backup-XXX) + const oldTemporaryName = filename + ".backup-" + transactionId; + console.log( + prefix, + "Renaming old file", + niceFileName(filename), + "to", + niceFileName(oldTemporaryName) + ); + await fs.promises.rename(filename, oldTemporaryName); + + // now, rename the temporary file (.tmp-XXX) to the target + console.log( + prefix, + "Renaming the temporary file", + niceFileName(tempName), + "to the original", + niceFileName(filename) + ); + await fs.promises.rename(tempName, filename); + + // we are done now, try to create a backup, but don't fail if the backup fails + try { + // check if there is an old backup file + const backupFileName = filename + ".backup"; + if (fs.existsSync(backupFileName)) { + console.log(prefix, "Deleting old backup file", niceFileName(backupFileName)); + // delete the old backup + await fs.promises.unlink(backupFileName); + } + + // rename the old file to the new backup file + console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location"); + await fs.promises.rename(oldTemporaryName, backupFileName); + } catch (ex) { + console.error(prefix, "Failed to switch backup files:", ex); + } + }); +} + +async function performFsJob(job) { const fname = path.join(storePath, job.filename); switch (job.type) { @@ -165,38 +241,35 @@ function performFsJob(job) { }; } - let contents = ""; try { - contents = fs.readFileSync(fname, { encoding: "utf8" }); + const data = await fs.promises.readFile(fname, { encoding: "utf8" }); + return { + success: true, + data, + }; } catch (ex) { return { error: ex, }; } - - return { - success: true, - data: contents, - }; } case "write": { try { - fs.writeFileSync(fname, job.contents); + await writeFileSafe(fname, job.contents); + return { + success: true, + data: job.contents, + }; } catch (ex) { return { error: ex, }; } - - return { - success: true, - data: job.contents, - }; } case "delete": { try { - fs.unlinkSync(fname); + await fs.promises.unlink(fname); } catch (ex) { return { error: ex, @@ -214,15 +287,10 @@ function performFsJob(job) { } } -ipcMain.on("fs-job", (event, arg) => { - const result = performFsJob(arg); +ipcMain.on("fs-job", async (event, arg) => { + const result = await performFsJob(arg); event.reply("fs-response", { id: arg.id, result }); }); -ipcMain.on("fs-sync-job", (event, arg) => { - const result = performFsJob(arg); - event.returnValue = result; -}); - steam.init(isDev); steam.listen(); diff --git a/electron/package.json b/electron/package.json index 9b5a99d2..79a6536b 100644 --- a/electron/package.json +++ b/electron/package.json @@ -14,5 +14,8 @@ }, "optionalDependencies": { "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v85" + }, + "dependencies": { + "async-lock": "^1.2.8" } } diff --git a/electron/yarn.lock b/electron/yarn.lock index 06a6bf25..f3101361 100644 --- a/electron/yarn.lock +++ b/electron/yarn.lock @@ -35,6 +35,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-12.20.5.tgz" integrity sha512-5Oy7tYZnu3a4pnJ//d4yVvOImExl4Vtwf0D40iKUlU+XlUsyV9iyFWyCFlwy489b72FMAik/EFwRkNLjjOdSPg== +async-lock@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c" + integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ== + boolean@^3.0.1: version "3.0.2" resolved "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz" diff --git a/src/js/platform/browser/storage.js b/src/js/platform/browser/storage.js index 2a399e54..ac0fa4ca 100644 --- a/src/js/platform/browser/storage.js +++ b/src/js/platform/browser/storage.js @@ -58,11 +58,6 @@ export class StorageImplBrowser extends StorageInterface { }); } - writeFileSyncIfSupported(filename, contents) { - window.localStorage.setItem(filename, contents); - return true; - } - readFileAsync(filename) { if (this.currentBusyFilename === filename) { logger.warn("Attempt to read", filename, "while write progress on it is ongoing!"); diff --git a/src/js/platform/browser/storage_indexed_db.js b/src/js/platform/browser/storage_indexed_db.js index 0c1dbe4f..1028eed3 100644 --- a/src/js/platform/browser/storage_indexed_db.js +++ b/src/js/platform/browser/storage_indexed_db.js @@ -94,12 +94,6 @@ export class StorageImplBrowserIndexedDB extends StorageInterface { }); } - writeFileSyncIfSupported(filename, contents) { - // Not supported - this.writeFileAsync(filename, contents); - return true; - } - readFileAsync(filename) { if (!this.database) { return Promise.reject("Storage not ready"); diff --git a/src/js/platform/electron/storage.js b/src/js/platform/electron/storage.js index 7736fcb4..41ed1746 100644 --- a/src/js/platform/electron/storage.js +++ b/src/js/platform/electron/storage.js @@ -46,14 +46,6 @@ export class StorageImplElectron extends StorageInterface { }); } - writeFileSyncIfSupported(filename, contents) { - return getIPCRenderer().sendSync("fs-sync-job", { - type: "write", - filename, - contents, - }); - } - readFileAsync(filename) { return new Promise((resolve, reject) => { // ipcMain diff --git a/src/js/platform/storage.js b/src/js/platform/storage.js index 1c7fc54b..165ee828 100644 --- a/src/js/platform/storage.js +++ b/src/js/platform/storage.js @@ -30,16 +30,6 @@ export class StorageInterface { return Promise.reject(); } - /** - * Tries to write a file synchronously, used in unload handler - * @param {string} filename - * @param {string} contents - */ - writeFileSyncIfSupported(filename, contents) { - abstract; - return false; - } - /** * Reads a string asynchronously. Returns Promise if file was not found. * @param {string} filename