ELECTRON: Fix issues in edge cases of using --export-save (#2590)

This commit is contained in:
catloversg
2026-04-03 13:57:25 +07:00
committed by GitHub
parent 8dcccdc5bb
commit abdf3082ca
5 changed files with 103 additions and 41 deletions

View File

@@ -25,5 +25,77 @@
<div> <div>
<h1>Close me when operation is completed.</h1> <h1>Close me when operation is completed.</h1>
</div> </div>
<!-- Use esm for top-level await -->
<script type="module">
const databaseName = "bitburnerSave";
// Check src/db.ts to see why the current max version is 2. If the database version is greater than this value, it
// means that the code in this file is outdated.
const maxDatabaseVersion = 2;
const databases = await window.indexedDB.databases();
const database = databases.find((info) => info.name === databaseName);
if (!database) {
alert("There is no save data");
// This is the simplest way to stop execution in top-level code without using a labeled block or IIFE.
throw new Error("There is no save data");
}
if (database.version === undefined || database.version > maxDatabaseVersion) {
alert(`Invalid database version: ${database.version}`);
throw new Error(`Invalid database version: ${database.version}`);
}
// Do NOT specify the version. We must open the database at the current version; otherwise, we will trigger
// onupgradeneeded.
const dbRequest = window.indexedDB.open(databaseName);
dbRequest.onerror = (event) => {
console.error(event.target.error);
alert(event.target.error);
};
dbRequest.onsuccess = () => {
const db = dbRequest.result;
try {
if (!db.objectStoreNames.contains("savestring")) {
alert("There is no save data");
return;
}
const transaction = db.transaction(["savestring"], "readonly");
const objectStore = transaction.objectStore("savestring");
const request = objectStore.get("save");
request.onsuccess = () => {
if (request.result == null) {
alert("There is no save data");
return;
}
let isBinaryFormat;
if (request.result instanceof Uint8Array) {
// All modules in the Electron folder are CommonJS, so importing them here would be really difficult. The
// isBinaryFormat function is very small, so let's inline it here.
isBinaryFormat = true;
const magicBytesOfDeflateGzip = [0x1f, 0x8b, 0x08];
for (let i = 0; i < magicBytesOfDeflateGzip.length; ++i) {
if (magicBytesOfDeflateGzip[i] !== request.result[i]) {
isBinaryFormat = false;
break;
}
}
} else {
isBinaryFormat = false;
}
const extension = isBinaryFormat ? "json.gz" : "json";
const filename = `bitburnerSave_${Date.now()}.${extension}`;
const blob = new Blob([request.result]);
const anchorElement = document.createElement("a");
const url = URL.createObjectURL(blob);
anchorElement.href = url;
anchorElement.download = filename;
anchorElement.click();
setTimeout(function () {
URL.revokeObjectURL(url);
}, 0);
};
} catch (error) {
console.error(error);
alert(error);
}
};
</script>
</body> </body>
</html> </html>

View File

@@ -258,7 +258,6 @@ app.on("ready", async () => {
await window.loadFile("export.html"); await window.loadFile("export.html");
window.show(); window.show();
setStopProcessHandler(window); setStopProcessHandler(window);
await utils.exportSave(window);
} else { } else {
window = await startWindow(process.argv.includes("--no-scripts")); window = await startWindow(process.argv.includes("--no-scripts"));
if (global.steamworksError) { if (global.steamworksError) {

View File

@@ -90,36 +90,6 @@ function showErrorBox(title, error) {
dialog.showErrorBox(title, `${error.name}\n\n${error.message}`); dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
} }
function exportSaveFromIndexedDb() {
return new Promise((resolve) => {
const dbRequest = indexedDB.open("bitburnerSave");
dbRequest.onsuccess = () => {
const db = dbRequest.result;
const transaction = db.transaction(["savestring"], "readonly");
const store = transaction.objectStore("savestring");
const request = store.get("save");
request.onsuccess = () => {
const file = new Blob([request.result], { type: "text/plain" });
const a = document.createElement("a");
const url = URL.createObjectURL(file);
a.href = url;
a.download = "save.json";
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
resolve();
}, 0);
};
};
});
}
async function exportSave(window) {
await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
}
async function writeTerminal(window, message, type = null) { async function writeTerminal(window, message, type = null) {
await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true); await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
} }
@@ -186,7 +156,6 @@ function initializeLogLevelConfig() {
module.exports = { module.exports = {
reloadAndKill, reloadAndKill,
showErrorBox, showErrorBox,
exportSave,
attachUnresponsiveAppHandler, attachUnresponsiveAppHandler,
detachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
writeTerminal, writeTerminal,

View File

@@ -1,5 +1,12 @@
import type { SaveData } from "./types"; import type { SaveData } from "./types";
export class IndexedDBVersionError extends Error {
constructor(message: string, options: ErrorOptions) {
super(message, options);
this.name = this.constructor.name;
}
}
function getDB(): Promise<IDBObjectStore> { function getDB(): Promise<IDBObjectStore> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!window.indexedDB) { if (!window.indexedDB) {
@@ -9,18 +16,29 @@ function getDB(): Promise<IDBObjectStore> {
* DB is called bitburnerSave * DB is called bitburnerSave
* Object store is called savestring * Object store is called savestring
* key for the Object store is called save * key for the Object store is called save
* Version `1` is important * Version `2` is important. When increasing the version, remember to update the code in electron/export.html.
*
* Version 1 is the initial version. We found a bug that caused the database to be missing the expected object
* store. In order to add the missing object store, we need to either increase the database version or delete and
* recreate the database. Increasing the version number is simpler. For more information, please check
* https://github.com/bitburner-official/bitburner-src/pull/2590
*/ */
const indexedDbRequest: IDBOpenDBRequest = window.indexedDB.open("bitburnerSave", 1); const indexedDbRequest: IDBOpenDBRequest = window.indexedDB.open("bitburnerSave", 2);
// This is called when there's no db to begin with. It's important, don't remove it. // This is called when there's no db to begin with. It's important, don't remove it.
indexedDbRequest.onupgradeneeded = function (this: IDBRequest<IDBDatabase>) { indexedDbRequest.onupgradeneeded = function (this: IDBRequest<IDBDatabase>) {
const db = this.result; const db = this.result;
if (db.objectStoreNames.contains("savestring")) {
return;
}
db.createObjectStore("savestring"); db.createObjectStore("savestring");
}; };
indexedDbRequest.onerror = function (this: IDBRequest<IDBDatabase>) { indexedDbRequest.onerror = function (this: IDBRequest<IDBDatabase>) {
reject(new Error("Failed to get IDB", { cause: this.error })); if (this.error?.name === "VersionError") {
reject(new IndexedDBVersionError(this.error.message, { cause: this.error }));
}
reject(this.error ?? new Error("Failed to get IDB"));
}; };
indexedDbRequest.onsuccess = function (this: IDBRequest<IDBDatabase>) { indexedDbRequest.onsuccess = function (this: IDBRequest<IDBDatabase>) {

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
import { Typography, Link, Button, ButtonGroup, Tooltip, Box, Paper, TextField } from "@mui/material"; import { Typography, Link, Button, ButtonGroup, Tooltip, Box, Paper, TextField } from "@mui/material";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { load } from "../../db"; import { IndexedDBVersionError, load } from "../../db";
import { Router } from "../GameRoot"; import { Router } from "../GameRoot";
import { Page } from "../Router"; import { Page } from "../Router";
import { type CrashReport, newIssueUrl, getCrashReport, isSaveDataFromNewerVersions } from "../../utils/ErrorHelper"; import { type CrashReport, newIssueUrl, getCrashReport, isSaveDataFromNewerVersions } from "../../utils/ErrorHelper";
@@ -112,14 +112,18 @@ export function RecoveryRoot({ softReset, crashReport, resetError }: IProps): Re
</Typography> </Typography>
); );
} else if ( } else if (
sourceError instanceof JSONReviverError && (sourceError instanceof JSONReviverError && isSaveDataFromNewerVersions(loadedSaveObjectMiniDump.VersionSave)) ||
isSaveDataFromNewerVersions(loadedSaveObjectMiniDump.VersionSave) sourceError instanceof IndexedDBVersionError
) { ) {
instructions = ( instructions = (
<Typography variant="h5" color={Settings.theme.warning}> <Typography variant="h5" color={Settings.theme.warning}>
{loadedSaveObjectMiniDump.VersionSave !== undefined && (
<>
Your save data is from a newer version (Version number: {loadedSaveObjectMiniDump.VersionSave}). The current Your save data is from a newer version (Version number: {loadedSaveObjectMiniDump.VersionSave}). The current
version number is {CONSTANTS.VersionNumber}. version number is {CONSTANTS.VersionNumber}.
<br /> <br />
</>
)}
Please check if you are using the correct build. This may happen when you load the save data of the dev build Please check if you are using the correct build. This may happen when you load the save data of the dev build
(Steam Beta or https://bitburner-official.github.io/bitburner-src) on the stable build. (Steam Beta or https://bitburner-official.github.io/bitburner-src) on the stable build.
</Typography> </Typography>