Skip to content

Commit 5dddb84

Browse files
committed
Attempt #1 on preserving savegame files
1 parent 71d5b02 commit 5dddb84

7 files changed

Lines changed: 100 additions & 53 deletions

File tree

electron/index.js

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
const { app, BrowserWindow, Menu, MenuItem, session } = require("electron");
44
const path = require("path");
55
const url = require("url");
6-
const childProcess = require("child_process");
76
const { ipcMain, shell } = require("electron");
87
const fs = require("fs");
9-
const steam = require('./steam');
8+
const steam = require("./steam");
9+
const asyncLock = require("async-lock");
10+
1011
const isDev = process.argv.indexOf("--dev") >= 0;
1112
const isLocal = process.argv.indexOf("--local") >= 0;
1213

@@ -153,7 +154,82 @@ ipcMain.on("exit-app", (event, flag) => {
153154
app.quit();
154155
});
155156

156-
function performFsJob(job) {
157+
let renameCounter = 1;
158+
159+
const fileLock = new asyncLock({
160+
timeout: 30000,
161+
maxPending: 1000,
162+
});
163+
164+
function niceFileName(filename) {
165+
return filename.replace(storePath, "@");
166+
}
167+
168+
async function writeFileSafe(filename, contents) {
169+
++renameCounter;
170+
const prefix = "[ " + renameCounter + ":" + niceFileName(filename) + " ] ";
171+
const transactionId = String(new Date().getTime()) + "." + renameCounter;
172+
173+
if (fileLock.isBusy()) {
174+
console.warn(prefix, "Concurrent write process on", filename);
175+
}
176+
177+
fileLock.acquire(filename, async () => {
178+
console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId);
179+
180+
if (!fs.existsSync(filename)) {
181+
// this one is easy
182+
console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename));
183+
await fs.promises.writeFile(filename, contents, { encoding: "utf8" });
184+
return;
185+
}
186+
187+
// first, write a temporary file (.tmp-XXX)
188+
const tempName = filename + ".tmp-" + transactionId;
189+
console.log(prefix, "Writing temporary file", niceFileName(tempName));
190+
await fs.promises.writeFile(tempName, contents, { encoding: "utf8" });
191+
192+
// now, rename the original file to (.backup-XXX)
193+
const oldTemporaryName = filename + ".backup-" + transactionId;
194+
console.log(
195+
prefix,
196+
"Renaming old file",
197+
niceFileName(filename),
198+
"to",
199+
niceFileName(oldTemporaryName)
200+
);
201+
await fs.promises.rename(filename, oldTemporaryName);
202+
203+
// now, rename the temporary file (.tmp-XXX) to the target
204+
console.log(
205+
prefix,
206+
"Renaming the temporary file",
207+
niceFileName(tempName),
208+
"to the original",
209+
niceFileName(filename)
210+
);
211+
await fs.promises.rename(tempName, filename);
212+
213+
// we are done now, try to create a backup, but don't fail if the backup fails
214+
try {
215+
// check if there is an old backup file
216+
const backupFileName = filename + ".backup";
217+
if (fs.existsSync(backupFileName)) {
218+
console.log(prefix, "Deleting old backup file", niceFileName(backupFileName));
219+
// delete the old backup
220+
await fs.promises.unlink(backupFileName);
221+
}
222+
223+
// rename the old file to the new backup file
224+
console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location");
225+
await fs.promises.rename(oldTemporaryName, backupFileName);
226+
} catch (ex) {
227+
console.error(prefix, "Failed to switch backup files:", ex);
228+
}
229+
});
230+
}
231+
232+
async function performFsJob(job) {
157233
const fname = path.join(storePath, job.filename);
158234

159235
switch (job.type) {
@@ -165,38 +241,35 @@ function performFsJob(job) {
165241
};
166242
}
167243

168-
let contents = "";
169244
try {
170-
contents = fs.readFileSync(fname, { encoding: "utf8" });
245+
const data = await fs.promises.readFile(fname, { encoding: "utf8" });
246+
return {
247+
success: true,
248+
data,
249+
};
171250
} catch (ex) {
172251
return {
173252
error: ex,
174253
};
175254
}
176-
177-
return {
178-
success: true,
179-
data: contents,
180-
};
181255
}
182256
case "write": {
183257
try {
184-
fs.writeFileSync(fname, job.contents);
258+
await writeFileSafe(fname, job.contents);
259+
return {
260+
success: true,
261+
data: job.contents,
262+
};
185263
} catch (ex) {
186264
return {
187265
error: ex,
188266
};
189267
}
190-
191-
return {
192-
success: true,
193-
data: job.contents,
194-
};
195268
}
196269

197270
case "delete": {
198271
try {
199-
fs.unlinkSync(fname);
272+
await fs.promises.unlink(fname);
200273
} catch (ex) {
201274
return {
202275
error: ex,
@@ -214,15 +287,10 @@ function performFsJob(job) {
214287
}
215288
}
216289

217-
ipcMain.on("fs-job", (event, arg) => {
218-
const result = performFsJob(arg);
290+
ipcMain.on("fs-job", async (event, arg) => {
291+
const result = await performFsJob(arg);
219292
event.reply("fs-response", { id: arg.id, result });
220293
});
221294

222-
ipcMain.on("fs-sync-job", (event, arg) => {
223-
const result = performFsJob(arg);
224-
event.returnValue = result;
225-
});
226-
227295
steam.init(isDev);
228296
steam.listen();

electron/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@
1414
},
1515
"optionalDependencies": {
1616
"shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v85"
17+
},
18+
"dependencies": {
19+
"async-lock": "^1.2.8"
1720
}
1821
}

electron/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
resolved "https://registry.npmjs.org/@types/node/-/node-12.20.5.tgz"
3636
integrity sha512-5Oy7tYZnu3a4pnJ//d4yVvOImExl4Vtwf0D40iKUlU+XlUsyV9iyFWyCFlwy489b72FMAik/EFwRkNLjjOdSPg==
3737

38+
async-lock@^1.2.8:
39+
version "1.2.8"
40+
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c"
41+
integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ==
42+
3843
boolean@^3.0.1:
3944
version "3.0.2"
4045
resolved "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz"

src/js/platform/browser/storage.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@ export class StorageImplBrowser extends StorageInterface {
5858
});
5959
}
6060

61-
writeFileSyncIfSupported(filename, contents) {
62-
window.localStorage.setItem(filename, contents);
63-
return true;
64-
}
65-
6661
readFileAsync(filename) {
6762
if (this.currentBusyFilename === filename) {
6863
logger.warn("Attempt to read", filename, "while write progress on it is ongoing!");

src/js/platform/browser/storage_indexed_db.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,6 @@ export class StorageImplBrowserIndexedDB extends StorageInterface {
9494
});
9595
}
9696

97-
writeFileSyncIfSupported(filename, contents) {
98-
// Not supported
99-
this.writeFileAsync(filename, contents);
100-
return true;
101-
}
102-
10397
readFileAsync(filename) {
10498
if (!this.database) {
10599
return Promise.reject("Storage not ready");

src/js/platform/electron/storage.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,6 @@ export class StorageImplElectron extends StorageInterface {
4646
});
4747
}
4848

49-
writeFileSyncIfSupported(filename, contents) {
50-
return getIPCRenderer().sendSync("fs-sync-job", {
51-
type: "write",
52-
filename,
53-
contents,
54-
});
55-
}
56-
5749
readFileAsync(filename) {
5850
return new Promise((resolve, reject) => {
5951
// ipcMain

src/js/platform/storage.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,6 @@ export class StorageInterface {
3030
return Promise.reject();
3131
}
3232

33-
/**
34-
* Tries to write a file synchronously, used in unload handler
35-
* @param {string} filename
36-
* @param {string} contents
37-
*/
38-
writeFileSyncIfSupported(filename, contents) {
39-
abstract;
40-
return false;
41-
}
42-
4333
/**
4434
* Reads a string asynchronously. Returns Promise<FILE_NOT_FOUND> if file was not found.
4535
* @param {string} filename

0 commit comments

Comments
 (0)