mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
FEATURE: Add "Recent Errors" tab and improved error modal (#2169)
This commit is contained in:
committed by
GitHub
parent
cf72937faf
commit
18f84396e2
62
src/ErrorHandling/DisplayError.ts
Normal file
62
src/ErrorHandling/DisplayError.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Router } from "../ui/GameRoot";
|
||||
import { SimplePage } from "@enums";
|
||||
import { errorModalsAreSuppressed, ErrorRecord, ErrorState } from "./ErrorState";
|
||||
|
||||
let currentId = 0;
|
||||
|
||||
export const DisplayError = (
|
||||
message: string,
|
||||
errorType: string,
|
||||
scriptName = "",
|
||||
hostname: string = "",
|
||||
pid: number = -1,
|
||||
) => {
|
||||
const errorPageOpen = Router.page() === SimplePage.RecentErrors;
|
||||
if (!errorPageOpen) {
|
||||
ErrorState.UnreadErrors++;
|
||||
}
|
||||
const prior = findExistingErrorCopy(message, hostname);
|
||||
if (prior) {
|
||||
prior.occurrences++;
|
||||
prior.time = new Date();
|
||||
if (pid !== -1) {
|
||||
prior.pid = pid;
|
||||
}
|
||||
prior.server = hostname;
|
||||
prior.message = message;
|
||||
|
||||
updateActiveError(prior);
|
||||
} else {
|
||||
ErrorState.Errors.unshift({
|
||||
id: currentId++,
|
||||
server: hostname,
|
||||
errorType,
|
||||
scriptName,
|
||||
message,
|
||||
pid,
|
||||
occurrences: 1,
|
||||
time: new Date(),
|
||||
unread: !errorPageOpen,
|
||||
});
|
||||
while (ErrorState.Errors.length > 100) {
|
||||
ErrorState.Errors.pop();
|
||||
}
|
||||
updateActiveError(ErrorState.Errors[0]);
|
||||
}
|
||||
};
|
||||
|
||||
function findExistingErrorCopy(message: string, hostname: string): ErrorRecord | null {
|
||||
const serverAgnosticMessage = message.replaceAll(hostname, "<server>");
|
||||
return (
|
||||
ErrorState.Errors.find(
|
||||
(e) => e.message.replaceAll(e.server, "<server>") === serverAgnosticMessage || e.message === message,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function updateActiveError(error: ErrorRecord): void {
|
||||
if (!ErrorState.ActiveError && !errorModalsAreSuppressed()) {
|
||||
ErrorState.ActiveError = error;
|
||||
ErrorState.ErrorUpdate.emit(ErrorState.ActiveError);
|
||||
}
|
||||
}
|
||||
114
src/ErrorHandling/ErrorModal.tsx
Normal file
114
src/ErrorHandling/ErrorModal.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { Modal } from "../ui/React/Modal";
|
||||
import { Box, Button, Typography } from "@mui/material/";
|
||||
import { errorModalsAreSuppressed, type ErrorRecord, ErrorState, toggleSuppressErrorModals } from "./ErrorState";
|
||||
import { Router } from "../ui/GameRoot";
|
||||
import { SimplePage, ToastVariant } from "@enums";
|
||||
import { useRerender } from "../ui/React/hooks";
|
||||
import { OptionSwitch } from "../ui/React/OptionSwitch";
|
||||
import { LogBoxEvents } from "../ui/React/LogBoxManager";
|
||||
import { recentScripts } from "../Netscript/RecentScripts";
|
||||
import { SnackbarEvents } from "../ui/React/Snackbar";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
|
||||
export function ErrorModal(): React.ReactElement {
|
||||
const { classes } = useStyles();
|
||||
const rerender = useRerender();
|
||||
const [error, setError] = useState<ErrorRecord | null>(ErrorState.ActiveError);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (newError: ErrorRecord) => {
|
||||
if (newError.force || (Router.page() !== SimplePage.RecentErrors && !errorModalsAreSuppressed())) {
|
||||
setError(newError);
|
||||
rerender();
|
||||
} else {
|
||||
ErrorState.ActiveError = null;
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
return ErrorState.ErrorUpdate.subscribe(listener);
|
||||
}, [rerender]);
|
||||
|
||||
const onClose = (force = false): void => {
|
||||
ErrorState.ActiveError && (ErrorState.ActiveError.unread = false);
|
||||
|
||||
if (force || errorModalsAreSuppressed()) {
|
||||
ErrorState.ActiveError = null;
|
||||
setError(null);
|
||||
} else {
|
||||
const nextError = ErrorState.Errors.find((e) => e.unread) ?? null;
|
||||
ErrorState.ActiveError = nextError;
|
||||
setError(nextError);
|
||||
}
|
||||
ErrorState.UnreadErrors = ErrorState.Errors.filter((e) => e.unread).length;
|
||||
};
|
||||
|
||||
const viewLogs = (): void => {
|
||||
if (error === null) {
|
||||
return;
|
||||
}
|
||||
const recentScript = recentScripts.find((script) => script.runningScript.pid === error.pid);
|
||||
if (!recentScript) {
|
||||
SnackbarEvents.emit(`No recent script found with pid ${error.pid}`, ToastVariant.INFO, 2000);
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
LogBoxEvents.emit(recentScript.runningScript);
|
||||
};
|
||||
|
||||
const goToErrorPage = () => {
|
||||
onClose(true);
|
||||
Router.toPage(SimplePage.RecentErrors);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={!!error} onClose={() => onClose()}>
|
||||
{error && (
|
||||
<>
|
||||
<Typography component="div">
|
||||
<h2>{error.errorType} ERROR</h2>
|
||||
{/* Add a zero-width space after each slash to allow clean wrapping. */}
|
||||
<p style={{ whiteSpace: "pre-wrap" }}>{error.message.replaceAll("/", "/\u200B")}</p>
|
||||
<p>
|
||||
Script: {error.scriptName}
|
||||
<br />
|
||||
PID: {error.pid}
|
||||
</p>
|
||||
{!Settings.SuppressErrorModals && (
|
||||
<OptionSwitch
|
||||
checked={errorModalsAreSuppressed()}
|
||||
onChange={(newValue) => toggleSuppressErrorModals(newValue)}
|
||||
text="Suppress error modals (5 min)"
|
||||
tooltip={
|
||||
<>
|
||||
If this is set, no error modals will be shown for the next five minutes, and only log errors to the
|
||||
Recent Errors page.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
<Box className={classes.inlineFlexBox}>
|
||||
<Button onClick={() => onClose()}>Close</Button>
|
||||
<div>
|
||||
<Button disabled={error.pid === -1} onClick={viewLogs}>
|
||||
View Script Logs
|
||||
</Button>
|
||||
<Button onClick={goToErrorPage}>Errors Page</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => ({
|
||||
inlineFlexBox: {
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
}));
|
||||
35
src/ErrorHandling/ErrorState.tsx
Normal file
35
src/ErrorHandling/ErrorState.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
|
||||
export type ErrorRecord = {
|
||||
id: number;
|
||||
server: string;
|
||||
errorType: string;
|
||||
message: string;
|
||||
scriptName: string;
|
||||
pid: number;
|
||||
occurrences: number;
|
||||
time: Date;
|
||||
unread: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const ErrorState = {
|
||||
ErrorUpdate: new EventEmitter<[ErrorRecord]>(),
|
||||
ActiveError: null as ErrorRecord | null,
|
||||
Errors: [] as ErrorRecord[],
|
||||
UnreadErrors: 0,
|
||||
PreventModalsUntil: new Date(),
|
||||
};
|
||||
|
||||
export function errorModalsAreSuppressed(): boolean {
|
||||
return ErrorState.PreventModalsUntil.getTime() > Date.now();
|
||||
}
|
||||
|
||||
export function toggleSuppressErrorModals(newValue: boolean, indefinite = false): void {
|
||||
if (newValue) {
|
||||
ErrorState.PreventModalsUntil = new Date(indefinite ? new Date("3000-01-01") : Date.now() + 1000 * 60 * 5);
|
||||
ErrorState.ActiveError = null;
|
||||
} else {
|
||||
ErrorState.PreventModalsUntil = new Date();
|
||||
}
|
||||
}
|
||||
120
src/ErrorHandling/RecentErrorsPage.tsx
Normal file
120
src/ErrorHandling/RecentErrorsPage.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { type ErrorRecord, ErrorState } from "./ErrorState";
|
||||
import { useRerender } from "../ui/React/hooks";
|
||||
import { Typography, Tooltip } from "@mui/material";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
|
||||
export function RecentErrorsPage(): React.ReactElement {
|
||||
const rerender = useRerender();
|
||||
React.useEffect(() => {
|
||||
const clearSubscription = ErrorState.ErrorUpdate.subscribe(rerender);
|
||||
ErrorState.UnreadErrors = 0;
|
||||
return () => {
|
||||
clearSubscription();
|
||||
ErrorState.UnreadErrors = 0;
|
||||
};
|
||||
}, [rerender]);
|
||||
|
||||
useEffect(() => {
|
||||
ErrorState.Errors.forEach((error) => {
|
||||
error.unread = false; // Mark all errors as read when the page is loaded
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { classes } = useStyles();
|
||||
|
||||
const showError = (error: ErrorRecord): void => {
|
||||
ErrorState.ErrorUpdate.emit({ ...error, force: true });
|
||||
};
|
||||
|
||||
const formatMessage = (message: string): string => {
|
||||
/**
|
||||
* - Add a zero-width space after each slash to allow clean wrapping.
|
||||
* - Replace 2+ newline characters with only 1 newline character to reduce the number of empty lines.
|
||||
*/
|
||||
return message.replaceAll("/", "/\u200B").replaceAll(/\n{2,}/g, "\n");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography component="div" sx={{ height: "100vh", overflowY: "auto", scrollbarWidth: "thin" }}>
|
||||
<table className={classes.errorTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={classes.cellText}>Count</th>
|
||||
<th className={classes.cellText}>Type</th>
|
||||
<th className={classes.cellText}>Message</th>
|
||||
<th className={classes.cellText}>Script</th>
|
||||
<th className={classes.cellText}>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ErrorState.Errors.map((e, i) => (
|
||||
<tr key={i} className={classes.errorRow} onClick={() => showError(e)}>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.xsmall}>{e.occurrences}</div>
|
||||
</td>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.xsmall}>{e.errorType}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={classes.errorText} key={i}>
|
||||
{formatMessage(e.message)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.small}>
|
||||
<Tooltip title={<>{formatMessage(e.scriptName)}</>}>
|
||||
<div style={{ textOverflow: "ellipsis", overflow: "auto" }}>{formatMessage(e.scriptName)}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.xsmall}>{e.time.toLocaleString()}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
errorTable: {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
borderCollapse: "collapse",
|
||||
},
|
||||
errorRow: {
|
||||
borderTop: `1px solid ${theme.colors.button}`,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.colors.button,
|
||||
},
|
||||
},
|
||||
cellText: {
|
||||
verticalAlign: "top",
|
||||
padding: "4px",
|
||||
textAlign: "left",
|
||||
},
|
||||
errorText: {
|
||||
margin: "4px",
|
||||
color: "white",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "pre-wrap",
|
||||
lineClamp: "6",
|
||||
lineHeight: 1.1,
|
||||
overflowX: "auto",
|
||||
maxHeight: "200px",
|
||||
},
|
||||
xsmall: {
|
||||
maxWidth: "110px",
|
||||
fontSize: "14px",
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
small: {
|
||||
maxWidth: "200px",
|
||||
},
|
||||
}));
|
||||
@@ -3,8 +3,13 @@ import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { GameOptionsPage } from "./GameOptionsPage";
|
||||
import { Player } from "@player";
|
||||
import { toggleSuppressErrorModals } from "../../ErrorHandling/ErrorState";
|
||||
|
||||
export const GameplayPage = (): React.ReactElement => {
|
||||
const toggleSuppressErrorModalsSetting = (newValue: boolean): void => {
|
||||
Settings.SuppressErrorModals = newValue;
|
||||
toggleSuppressErrorModals(newValue, true);
|
||||
};
|
||||
return (
|
||||
<GameOptionsPage title="Gameplay">
|
||||
<OptionSwitch
|
||||
@@ -52,6 +57,17 @@ export const GameplayPage = (): React.ReactElement => {
|
||||
text="Suppress TIX messages"
|
||||
tooltip={<>If this is set, the stock market will never create any popup.</>}
|
||||
/>
|
||||
<OptionSwitch
|
||||
checked={Settings.SuppressErrorModals}
|
||||
onChange={toggleSuppressErrorModalsSetting}
|
||||
text="Suppress error modals"
|
||||
tooltip={
|
||||
<>
|
||||
If this is set, script errors will never create any popups. The errors can still be seen on the "Recent
|
||||
Errors" tab in the Active Scripts page.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{Player.bladeburner && (
|
||||
<OptionSwitch
|
||||
checked={Settings.SuppressBladeburnerPopup}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ScriptDeath } from "./ScriptDeath";
|
||||
import { WorkerScript } from "./WorkerScript";
|
||||
import { workerScripts } from "./WorkerScripts";
|
||||
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
import { GetAllServers, GetServer } from "../Server/AllServers";
|
||||
import { AddRecentScript } from "./RecentScripts";
|
||||
import { ITutorial } from "../InteractiveTutorial";
|
||||
import { AlertEvents } from "../ui/React/AlertManager";
|
||||
@@ -34,6 +34,16 @@ export function killWorkerScriptByPid(pid: number, killer?: WorkerScript): boole
|
||||
return false;
|
||||
}
|
||||
|
||||
export const killAllScripts = () => {
|
||||
for (const server of GetAllServers()) {
|
||||
for (const byPid of server.runningScriptMap.values()) {
|
||||
for (const pid of byPid.keys()) {
|
||||
killWorkerScriptByPid(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function stopAndCleanUpWorkerScript(ws: WorkerScript): void {
|
||||
// Only clean up once.
|
||||
// Important: Only this function can set stopFlag!
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
assertAndSanitizeStyles,
|
||||
} from "../JsonSchema/JSONSchemaAssertion";
|
||||
import { mergePlayerDefinedKeyBindings, type PlayerDefinedKeyBindingsType } from "../utils/KeyBindingUtils";
|
||||
import { toggleSuppressErrorModals } from "../ErrorHandling/ErrorState";
|
||||
|
||||
/**
|
||||
* This function won't be able to catch **all** invalid hostnames. In order to validate a hostname properly, we need to
|
||||
@@ -119,6 +120,8 @@ export const Settings = {
|
||||
SaveGameOnFileSave: true,
|
||||
/** Whether to hide the confirmation dialog for augmentation purchases. */
|
||||
SuppressBuyAugmentationConfirmation: false,
|
||||
/** Whether to hide the info dialog for script errors. */
|
||||
SuppressErrorModals: false,
|
||||
/** Whether to hide the dialog showing new faction invites. */
|
||||
SuppressFactionInvites: false,
|
||||
/** Whether to hide the dialog when the player receives a new message file. */
|
||||
@@ -253,5 +256,8 @@ export const Settings = {
|
||||
|
||||
// Merge Settings.KeyBindings with DefaultKeyBindings.
|
||||
mergePlayerDefinedKeyBindings(Settings.KeyBindings);
|
||||
|
||||
// Set up initial state for error modal suppression
|
||||
toggleSuppressErrorModals(Settings.SuppressErrorModals, true);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ export function SidebarAccordion({
|
||||
key_={key_}
|
||||
icon={icon}
|
||||
count={count}
|
||||
active={active ?? page === key_}
|
||||
active={active ?? (page === key_ || x.alternateKeys?.includes(page))}
|
||||
clickFn={getClickFn(clickPage, key_)}
|
||||
flash={flash === key_}
|
||||
classes={classes}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface ICreateProps {
|
||||
icon: React.ReactElement["type"];
|
||||
count?: number;
|
||||
active?: boolean;
|
||||
alternateKeys?: Page[];
|
||||
}
|
||||
|
||||
export interface SidebarItemProps extends ICreateProps {
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
CurrentKeyBindings,
|
||||
} from "../../utils/KeyBindingUtils";
|
||||
import { throwIfReachable } from "../../utils/helpers/throwIfReachable";
|
||||
import { ErrorState } from "../../ErrorHandling/ErrorState";
|
||||
|
||||
const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon(
|
||||
props: { color: "primary" | "secondary" | "error" },
|
||||
@@ -148,6 +149,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
const augmentationCount = Player.queuedAugmentations.length;
|
||||
const invitationsCount = Player.factionInvitations.filter((f) => !InvitationsSeen.has(f)).length;
|
||||
const programCount = getAvailableCreatePrograms().length - ProgramsSeen.size;
|
||||
const errorCount = ErrorState.UnreadErrors;
|
||||
|
||||
const canOpenFactions =
|
||||
Player.factionInvitations.length > 0 ||
|
||||
@@ -339,7 +341,12 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
items={[
|
||||
{ key_: Page.Terminal, icon: LastPageIcon },
|
||||
{ key_: Page.ScriptEditor, icon: CreateIcon },
|
||||
{ key_: Page.ActiveScripts, icon: StorageIcon },
|
||||
{
|
||||
key_: Page.ActiveScripts,
|
||||
icon: StorageIcon,
|
||||
count: errorCount,
|
||||
alternateKeys: [Page.RecentErrors, Page.RecentlyKilledScripts],
|
||||
},
|
||||
{ key_: Page.CreateProgram, icon: BugReportIcon, count: programCount },
|
||||
canStaneksGift && { key_: Page.StaneksGift, icon: DeveloperBoardIcon },
|
||||
]}
|
||||
|
||||
@@ -2,30 +2,95 @@
|
||||
* Root React Component for the "Active Scripts" UI page. This page displays
|
||||
* and provides information about all of the player's scripts that are currently running
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Tabs, Tab } from "@mui/material";
|
||||
|
||||
import { ActiveScriptsPage } from "./ActiveScriptsPage";
|
||||
import { RecentScriptsPage } from "./RecentScriptsPage";
|
||||
import { RecentErrorsPage } from "../../ErrorHandling/RecentErrorsPage";
|
||||
import { useRerender } from "../React/hooks";
|
||||
import { errorModalsAreSuppressed, ErrorState, toggleSuppressErrorModals } from "../../ErrorHandling/ErrorState";
|
||||
import { OptionSwitch } from "../React/OptionSwitch";
|
||||
import { killAllScripts } from "../../Netscript/killWorkerScript";
|
||||
import { SimplePage } from "@enums";
|
||||
import { Router } from "../GameRoot";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
|
||||
export function ActiveScriptsRoot(): React.ReactElement {
|
||||
const [tab, setTab] = useState<"active" | "recent">("active");
|
||||
type ActiveScriptsTab = SimplePage.ActiveScripts | SimplePage.RecentlyKilledScripts | SimplePage.RecentErrors;
|
||||
|
||||
export type ComponentProps = {
|
||||
page: ActiveScriptsTab;
|
||||
};
|
||||
|
||||
export function ActiveScriptsRoot({ page }: ComponentProps): React.ReactElement {
|
||||
const [tab, setTab] = useState<ActiveScriptsTab>(page);
|
||||
useRerender(400);
|
||||
|
||||
function handleChange(event: React.SyntheticEvent, tab: "active" | "recent"): void {
|
||||
useEffect(() => {
|
||||
if (ErrorState.UnreadErrors > 0) {
|
||||
handleChange(null, SimplePage.RecentErrors);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function handleChange(
|
||||
__event: React.SyntheticEvent | null,
|
||||
tab: SimplePage.ActiveScripts | SimplePage.RecentlyKilledScripts | SimplePage.RecentErrors,
|
||||
): void {
|
||||
setTab(tab);
|
||||
Router.toPage(tab);
|
||||
}
|
||||
|
||||
function errorTabText(): string {
|
||||
if (!ErrorState.UnreadErrors || tab === SimplePage.RecentErrors) {
|
||||
return "Recent Errors";
|
||||
}
|
||||
return `Recent Errors (${ErrorState.UnreadErrors})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs variant="fullWidth" value={tab} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "25%" }}>
|
||||
<Tab label={"Active"} value={"active"} />
|
||||
<Tab label={"Recently Killed"} value={"recent"} />
|
||||
</Tabs>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
minHeight: "fit-content",
|
||||
"& .MuiButtonBase-root.MuiTab-root": {
|
||||
margin: 0,
|
||||
padding: "10px",
|
||||
whiteSpace: "pre",
|
||||
minHeight: "40px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab label={"Active"} value={SimplePage.ActiveScripts} />
|
||||
<Tab label={"Recently Killed"} value={SimplePage.RecentlyKilledScripts} />
|
||||
<Tab label={errorTabText()} value={SimplePage.RecentErrors} />
|
||||
</Tabs>
|
||||
{Settings.SuppressErrorModals ? (
|
||||
<div style={{ width: "15%" }}></div>
|
||||
) : (
|
||||
<OptionSwitch
|
||||
checked={errorModalsAreSuppressed()}
|
||||
onChange={(newValue) => toggleSuppressErrorModals(newValue)}
|
||||
text="Suppress error modals (5 min)"
|
||||
tooltip={
|
||||
<>
|
||||
If this is set, no error modals will be shown for the next five minutes, and only log errors to the
|
||||
Recent Errors page.
|
||||
</>
|
||||
}
|
||||
wrapperStyles={{ marginLeft: "20px" }}
|
||||
/>
|
||||
)}
|
||||
<Button color="error" onClick={killAllScripts} sx={{ margin: 0 }}>
|
||||
Kill All Scripts
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{tab === "active" && <ActiveScriptsPage />}
|
||||
{tab === "recent" && <RecentScriptsPage />}
|
||||
{tab === SimplePage.ActiveScripts && <ActiveScriptsPage />}
|
||||
{tab === SimplePage.RecentlyKilledScripts && <RecentScriptsPage />}
|
||||
{tab === SimplePage.RecentErrors && <RecentErrorsPage />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export enum ToastVariant {
|
||||
*/
|
||||
export enum SimplePage {
|
||||
ActiveScripts = "Active Scripts",
|
||||
RecentlyKilledScripts = "Recently Killed Scripts",
|
||||
RecentErrors = "Recent Errors",
|
||||
Augmentations = "Augmentations",
|
||||
Bladeburner = "Bladeburner",
|
||||
City = "City",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
@@ -7,7 +7,7 @@ import { Player } from "@player";
|
||||
import { installAugmentations } from "../Augmentation/AugmentationHelpers";
|
||||
import { saveObject } from "../SaveObject";
|
||||
import { onExport } from "../ExportBonus";
|
||||
import { CompletedProgramName, LocationName } from "@enums";
|
||||
import { CompletedProgramName, LocationName, SimplePage } from "@enums";
|
||||
import { ITutorial, iTutorialStart } from "../InteractiveTutorial";
|
||||
import { InteractiveTutorialRoot } from "./InteractiveTutorial/InteractiveTutorialRoot";
|
||||
import { ITutorialEvents } from "./InteractiveTutorial/ITutorialEvents";
|
||||
@@ -18,7 +18,7 @@ import { GetAllServers } from "../Server/AllServers";
|
||||
import { StockMarket } from "../StockMarket/StockMarket";
|
||||
|
||||
import type { ComplexPage } from "./Enums";
|
||||
import type { PageWithContext, IRouter, PageContext } from "./Router";
|
||||
import type { IRouter, PageContext, PageWithContext } from "./Router";
|
||||
import { Page } from "./Router";
|
||||
import { Overview } from "./React/Overview";
|
||||
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
|
||||
@@ -78,6 +78,7 @@ import { Settings } from "../Settings/Settings";
|
||||
import { isBitNodeFinished } from "../BitNode/BitNodeUtils";
|
||||
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
||||
import { SpecialServers } from "../Server/data/SpecialServers";
|
||||
import { ErrorModal } from "../ErrorHandling/ErrorModal";
|
||||
import { DocumentationPopUp } from "../Documentation/ui/DocumentationPopUp";
|
||||
|
||||
const htmlLocation = location;
|
||||
@@ -281,7 +282,15 @@ export function GameRoot(): React.ReactElement {
|
||||
break;
|
||||
}
|
||||
case Page.ActiveScripts: {
|
||||
mainPage = <ActiveScriptsRoot />;
|
||||
mainPage = <ActiveScriptsRoot page={SimplePage.ActiveScripts} />;
|
||||
break;
|
||||
}
|
||||
case Page.RecentlyKilledScripts: {
|
||||
mainPage = <ActiveScriptsRoot page={SimplePage.RecentlyKilledScripts} />;
|
||||
break;
|
||||
}
|
||||
case Page.RecentErrors: {
|
||||
mainPage = <ActiveScriptsRoot page={SimplePage.RecentErrors} />;
|
||||
break;
|
||||
}
|
||||
case Page.Hacknet: {
|
||||
@@ -441,6 +450,7 @@ export function GameRoot(): React.ReactElement {
|
||||
<Unclickable />
|
||||
<LogBoxManager hidden={hidePopups} />
|
||||
<AlertManager hidden={hidePopups} />
|
||||
<ErrorModal />
|
||||
<PromptManager hidden={hidePopups} />
|
||||
<FactionInvitationManager hidden={hidePopups} />
|
||||
<Snackbar hidden={hidePopups} />
|
||||
|
||||
@@ -7,6 +7,7 @@ type OptionSwitchProps = {
|
||||
onChange: (newValue: boolean, error?: string) => void;
|
||||
text: React.ReactNode;
|
||||
tooltip: React.ReactNode;
|
||||
wrapperStyles?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function OptionSwitch({
|
||||
@@ -15,6 +16,7 @@ export function OptionSwitch({
|
||||
onChange,
|
||||
text,
|
||||
tooltip,
|
||||
wrapperStyles,
|
||||
}: OptionSwitchProps): React.ReactElement {
|
||||
const [value, setValue] = useState(checked);
|
||||
|
||||
@@ -29,7 +31,7 @@ export function OptionSwitch({
|
||||
}, [checked]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={wrapperStyles}>
|
||||
<FormControlLabel
|
||||
disabled={disabled}
|
||||
control={<Switch checked={value} onChange={handleSwitchChange} />}
|
||||
@@ -39,7 +41,6 @@ export function OptionSwitch({
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { basicErrorMessage } from "../Netscript/ErrorMessages";
|
||||
import { ScriptDeath } from "../Netscript/ScriptDeath";
|
||||
import type { WorkerScript } from "../Netscript/WorkerScript";
|
||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
||||
import { getErrorMessageWithStackAndCause } from "./ErrorHelper";
|
||||
import { getErrorMessageWithStackAndCause, parseUnknownError } from "./ErrorHelper";
|
||||
|
||||
import { DisplayError } from "../ErrorHandling/DisplayError";
|
||||
|
||||
/** Generate an error dialog when workerscript is known */
|
||||
export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, initialText = "") {
|
||||
@@ -10,12 +11,36 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, i
|
||||
// No dialog for ScriptDeath
|
||||
return;
|
||||
}
|
||||
const errorDetails = parseUnknownError(e);
|
||||
if (ws && typeof e === "string") {
|
||||
const headerText = basicErrorMessage(ws, "", "");
|
||||
if (!e.includes(headerText)) e = basicErrorMessage(ws, e);
|
||||
/**
|
||||
* - Attempt to strip out the error type, if present.
|
||||
* - Extract error text by skipping:
|
||||
* - Error type
|
||||
* - Script name and PID
|
||||
*
|
||||
* Error example:
|
||||
* "RUNTIME ERROR\ntest.js@home (PID - 1)\n\ngetServer: Invalid hostname: 'invalid'\n\nStack:\ntest.js:L5@main"
|
||||
*
|
||||
* - errorType: "RUNTIME"
|
||||
* - errorText: "getServer: Invalid hostname: 'invalid'\n\nStack:\ntest.js:L5@main"
|
||||
*/
|
||||
const errorType = e.match(/^(\w+) ERROR/)?.[1];
|
||||
if (errorType) {
|
||||
const errorText = e.split(/\n/).slice(3).join("\n");
|
||||
DisplayError(initialText + errorText, errorType, ws.scriptRef.filename, ws.hostname, ws.pid);
|
||||
return;
|
||||
}
|
||||
DisplayError(initialText + e, "RUNTIME", ws.scriptRef.filename, ws.hostname, ws.pid);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const msg = `${e.message} (sorry we can't be more helpful)`;
|
||||
e = ws ? basicErrorMessage(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`;
|
||||
DisplayError(
|
||||
initialText + msg + (errorDetails.stack ?? ""),
|
||||
"SYNTAX",
|
||||
ws?.scriptRef?.filename,
|
||||
ws?.hostname,
|
||||
ws?.pid,
|
||||
);
|
||||
} else if (e instanceof Error) {
|
||||
// Ignore any cancellation errors from Monaco that get here
|
||||
if (e.name === "Canceled" && e.message === "Canceled") {
|
||||
@@ -32,15 +57,13 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, i
|
||||
*/
|
||||
console.error(e);
|
||||
const msg = getErrorMessageWithStackAndCause(e);
|
||||
e = ws ? basicErrorMessage(ws, msg) : `RUNTIME ERROR:\n\n${msg}`;
|
||||
}
|
||||
if (typeof e !== "string") {
|
||||
DisplayError(initialText + msg, getErrorType(e.stack) ?? "RUNTIME", ws?.scriptRef?.filename, ws?.hostname, ws?.pid);
|
||||
} else if (typeof e !== "string") {
|
||||
console.error("Unexpected error:", e);
|
||||
const msg = `Unexpected type of error thrown. This error was likely thrown manually within a script.
|
||||
Error has been logged to the console.\n\nType of error: ${typeof e}\nValue of error: ${e}`;
|
||||
e = ws ? basicErrorMessage(ws, msg, "UNKNOWN") : msg;
|
||||
DisplayError(msg, "UNKNOWN", ws?.scriptRef?.filename, ws?.hostname, ws?.pid);
|
||||
}
|
||||
dialogBoxCreate(initialText + String(e));
|
||||
}
|
||||
|
||||
/** Use this handler to handle the error when we call getSaveData function or getSaveInfo function */
|
||||
@@ -55,3 +78,22 @@ export function handleGetSaveDataInfoError(error: unknown, fromGetSaveInfo = fal
|
||||
}
|
||||
dialogBoxCreate(errorMessage);
|
||||
}
|
||||
|
||||
function getErrorType(e = ""): string | undefined {
|
||||
if (e.toLowerCase().includes("typeerror")) {
|
||||
return "TYPE";
|
||||
}
|
||||
if (e.toLowerCase().includes("syntaxerror")) {
|
||||
return "SYNTAX";
|
||||
}
|
||||
if (e.toLowerCase().includes("referenceerror")) {
|
||||
return "REFERENCE";
|
||||
}
|
||||
if (e.toLowerCase().includes("rangeerror")) {
|
||||
return "RANGE";
|
||||
}
|
||||
|
||||
// Check if the first line contains an error type
|
||||
const match = e.match(/^\s*([A-Z]+)\s+ERROR/);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export function getErrorMessageWithStackAndCause(error: unknown, prefix = ""): s
|
||||
const errorData = parseUnknownError(error);
|
||||
let errorMessage = `${prefix}${errorData.errorAsString}`;
|
||||
if (errorData.stack) {
|
||||
errorMessage += `\nStack: ${errorData.stack}`;
|
||||
errorMessage += `\n\nStack: ${errorData.stack}`;
|
||||
}
|
||||
if (errorData.causeAsString) {
|
||||
errorMessage += `\nError cause: ${errorData.causeAsString}`;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SpecialServers } from "../../../src/Server/data/SpecialServers";
|
||||
import { WorkerScript } from "../../../src/Netscript/WorkerScript";
|
||||
import { NetscriptFunctions } from "../../../src/NetscriptFunctions";
|
||||
import type { PositiveInteger } from "../../../src/types";
|
||||
import { ErrorState } from "../../../src/ErrorHandling/ErrorState";
|
||||
|
||||
declare const importActual: (typeof EvaluatorConfig)["doImport"];
|
||||
|
||||
@@ -118,6 +119,7 @@ const runOptions = {
|
||||
describe("runScript and runScriptFromScript", () => {
|
||||
let alertDelete: () => void;
|
||||
let alerted: Promise<unknown>;
|
||||
let errorShown: Promise<unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
setupBasicTestingEnvironment();
|
||||
@@ -127,6 +129,9 @@ describe("runScript and runScriptFromScript", () => {
|
||||
alerted = new Promise((resolve) => {
|
||||
alertDelete = AlertEvents.subscribe((x) => resolve(x));
|
||||
});
|
||||
errorShown = new Promise((resolve) => {
|
||||
ErrorState.ErrorUpdate.subscribe((x) => resolve(x));
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
alertDelete();
|
||||
@@ -215,7 +220,7 @@ describe("runScript and runScriptFromScript", () => {
|
||||
throw new Error(`Invalid worker script`);
|
||||
}
|
||||
const result = await Promise.race([
|
||||
alerted,
|
||||
errorShown,
|
||||
new Promise<void>((resolve) => (workerScript.atExit = new Map([["default", resolve]]))),
|
||||
]);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user