mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-02 13:57:05 +02:00
MISC: Export crash report when a fatal error occurs (#2106)
This commit is contained in:
@@ -35,6 +35,19 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
|
||||
console.error(error, errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* When an error is thrown, this function is called twice and renders RecoveryRoot with two different errorData, even
|
||||
* when there is only one error. The flow is roughly like this:
|
||||
* - The error is thrown.
|
||||
* - getDerivedStateFromError() -> Set hasError and error
|
||||
* - render() -> Render RecoveryRoot with errorData1, which does not contain errorInfo and page
|
||||
* - componentDidCatch() -> Set errorInfo and page
|
||||
* - render() -> Render RecoveryRoot with errorData2, which contains errorInfo and page
|
||||
*
|
||||
* This means that if we use useEffect(()=>{}, [errorData]) in RecoveryRoot, that hook will be called twice with 2
|
||||
* different errorData. The second errorData, which contains errorInfo and page, is the "final" value that is shown on
|
||||
* the recovery screen.
|
||||
*/
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
let errorData: IErrorData | undefined;
|
||||
|
||||
+13
-4
@@ -61,7 +61,7 @@ import { AlertManager } from "./React/AlertManager";
|
||||
import { PromptManager } from "./React/PromptManager";
|
||||
import { FactionInvitationManager } from "../Faction/ui/FactionInvitationManager";
|
||||
import { calculateAchievements } from "../Achievements/Achievements";
|
||||
import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
|
||||
import { ActivateRecoveryMode, RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
|
||||
import { AchievementsRoot } from "../Achievements/AchievementsRoot";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { ThemeBrowser } from "../Themes/ui/ThemeBrowser";
|
||||
@@ -116,9 +116,18 @@ function determineStartPage(): PageWithContext {
|
||||
if (RecoveryMode) {
|
||||
return { page: Page.Recovery };
|
||||
}
|
||||
if (isBitNodeFinished()) {
|
||||
// Go to BitVerse UI without animation.
|
||||
return { page: Page.BitVerse, flume: false, quick: true };
|
||||
/**
|
||||
* If the save data contains the server list, but WD data is invalid, isBitNodeFinished() will throw an error, and the
|
||||
* main UI will show a black screen instead of the recovery screen.
|
||||
*/
|
||||
try {
|
||||
if (isBitNodeFinished()) {
|
||||
// Go to BitVerse UI without animation.
|
||||
return { page: Page.BitVerse, flume: false, quick: true };
|
||||
}
|
||||
} catch (error) {
|
||||
ActivateRecoveryMode(error);
|
||||
return { page: Page.Recovery };
|
||||
}
|
||||
if (Player.currentWork !== null) {
|
||||
return { page: Page.Work };
|
||||
|
||||
@@ -40,10 +40,9 @@ export function LoadingScreen(): React.ReactElement {
|
||||
pushGameReady();
|
||||
setLoaded(true);
|
||||
})
|
||||
.catch(async (error) => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
ActivateRecoveryMode(error);
|
||||
await Engine.load("");
|
||||
setLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -14,6 +14,8 @@ import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
import { isBinaryFormat } from "../../../electron/saveDataBinaryFormat";
|
||||
import { InvalidSaveData, UnsupportedSaveData } from "../../utils/SaveDataUtils";
|
||||
import { downloadContentAsFile } from "../../utils/FileUtils";
|
||||
import { debounce } from "lodash";
|
||||
import { Engine } from "../../engine";
|
||||
|
||||
export let RecoveryMode = false;
|
||||
let sourceError: unknown;
|
||||
@@ -32,9 +34,8 @@ interface IProps {
|
||||
function exportSaveFile(): void {
|
||||
load()
|
||||
.then((content) => {
|
||||
const epochTime = Math.round(Date.now() / 1000);
|
||||
const extension = isBinaryFormat(content) ? "json.gz" : "json";
|
||||
const filename = `RECOVERY_BITBURNER_${epochTime}.${extension}`;
|
||||
const filename = `RECOVERY_BITBURNER_${Date.now()}.${extension}`;
|
||||
downloadContentAsFile(content, filename);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -42,6 +43,25 @@ function exportSaveFile(): void {
|
||||
});
|
||||
}
|
||||
|
||||
const debouncedExportSaveFile = debounce(exportSaveFile, 1000);
|
||||
|
||||
const debouncedExportCrashReport = debounce((crashReport: unknown) => {
|
||||
const content = typeof crashReport === "object" ? JSON.stringify(crashReport) : String(crashReport);
|
||||
downloadContentAsFile(content, `CRASH_REPORT_BITBURNER_${Date.now()}.txt`);
|
||||
}, 2000);
|
||||
|
||||
/**
|
||||
* The recovery screen can be activated in 2 ways:
|
||||
* - Call ActivateRecoveryMode() [1].
|
||||
* - Before loading the save data: An error is thrown in src\ui\LoadingScreen.tsx (e.g., cannot load SWC wasm files,
|
||||
* cannot access IndexedDB and load the save data, Engine.load() throws an error).
|
||||
* - isBitNodeFinished() throws an error in src\ui\GameRoot.tsx.
|
||||
* - ErrorBoundary [2]: After loading the save data and GameRoot is rendered, an error is thrown anywhere else.
|
||||
*
|
||||
* [1]: errorData is undefined and sourceError, which is the error thrown in LoadingScreen.tsx, is set via ActivateRecoveryMode().
|
||||
* [2]: RecoveryRoot is rendered twice with 2 different errorData. For more information, please check the comment in
|
||||
* src\ui\ErrorBoundary.tsx.
|
||||
*/
|
||||
export function RecoveryRoot({ softReset, errorData, resetError }: IProps): React.ReactElement {
|
||||
function recover(): void {
|
||||
if (resetError) resetError();
|
||||
@@ -51,16 +71,28 @@ export function RecoveryRoot({ softReset, errorData, resetError }: IProps): Reac
|
||||
}
|
||||
Settings.AutosaveInterval = 0;
|
||||
|
||||
// The architecture around RecoveryRoot is awkward, and it can be invoked in
|
||||
// a number of ways. If we are invoked via a save error, sourceError will be set
|
||||
// and we won't have decoded the information into errorData.
|
||||
// This happens in [1] mentioned above. errorData is undefined, so we need to parse sourceError to get errorData.
|
||||
if (errorData == null && sourceError) {
|
||||
errorData = getErrorForDisplay(sourceError, undefined, Page.LoadingScreen);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
exportSaveFile();
|
||||
}, []);
|
||||
// This hook is called twice in [2], so we need to debounce exportSaveFile().
|
||||
debouncedExportSaveFile();
|
||||
|
||||
/**
|
||||
* This hook can be called with 3 types of errorData:
|
||||
* - In [1]: errorData.metadata.page is Page.LoadingScreen
|
||||
* - In [2]:
|
||||
* - First render: errorData.metadata.errorInfo is undefined
|
||||
* - Second render: errorData.metadata.errorInfo contains componentStack
|
||||
*
|
||||
* The following check makes sure that we do not write the crash report in the "first render" of [2].
|
||||
*/
|
||||
if (errorData && (errorData.metadata.errorInfo || errorData.metadata.page === Page.LoadingScreen)) {
|
||||
debouncedExportCrashReport(errorData.body);
|
||||
}
|
||||
}, [errorData]);
|
||||
|
||||
let instructions;
|
||||
if (sourceError instanceof UnsupportedSaveData) {
|
||||
@@ -88,11 +120,18 @@ export function RecoveryRoot({ softReset, errorData, resetError }: IProps): Reac
|
||||
Post in the #bug-report channel on Discord.
|
||||
</Link>
|
||||
</Typography>
|
||||
<Typography>Please include your save file.</Typography>
|
||||
<Typography>Please include your save file and the crash report.</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If Engine.isRunning is false, it means that the loading process in src\ui\LoadingScreen.tsx failed, and the loaded
|
||||
* data is either empty or corrupted (partially or fully). In this case, there is no reason to allow the player to
|
||||
* disable the recovery mode and go back to the main UI.
|
||||
*/
|
||||
const canDisableRecoveryMode = Engine.isRunning;
|
||||
|
||||
return (
|
||||
<Box sx={{ padding: "8px 16px", minHeight: "100vh", maxWidth: "1200px", boxSizing: "border-box" }}>
|
||||
<Typography variant="h3">RECOVERY MODE ACTIVATED</Typography>
|
||||
@@ -114,13 +153,19 @@ export function RecoveryRoot({ softReset, errorData, resetError }: IProps): Reac
|
||||
<Button onClick={exportSaveFile}>Export save file</Button>
|
||||
<br />
|
||||
<br />
|
||||
<Typography>You can disable the recovery mode, but the game may not work correctly.</Typography>
|
||||
{canDisableRecoveryMode && (
|
||||
<Typography>
|
||||
You can disable the recovery mode, but the game may not work correctly, and your save data may be corrupted.
|
||||
</Typography>
|
||||
)}
|
||||
<ButtonGroup sx={{ my: 2 }}>
|
||||
<Tooltip title="Disable the recovery mode and attempt to head back to the terminal page. This may or may not work. Ensure you saved the recovery file.">
|
||||
<Button onClick={recover} startIcon={<DirectionsRunIcon />}>
|
||||
Disable Recovery Mode
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{canDisableRecoveryMode && (
|
||||
<Tooltip title="Disable the recovery mode and attempt to head back to the terminal page. This may or may not work. Ensure you saved the recovery file.">
|
||||
<Button onClick={recover} startIcon={<DirectionsRunIcon />}>
|
||||
Disable Recovery Mode
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<SoftResetButton color="warning" onTriggered={softReset} />
|
||||
<DeleteGameButton color="error" />
|
||||
</ButtonGroup>
|
||||
|
||||
Reference in New Issue
Block a user