error boundary now properly reports steam errors

This commit is contained in:
AAGaming
2024-05-27 17:21:27 -04:00
parent a84a13c76d
commit 9c8db576f5
12 changed files with 247 additions and 169 deletions

View File

@@ -41,4 +41,4 @@ jobs:
- name: Run tsc (TypeScript) - name: Run tsc (TypeScript)
working-directory: frontend working-directory: frontend
run: $(pnpm bin)/tsc --noEmit run: pnpm run typecheck

View File

@@ -8,6 +8,7 @@ from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user
from typing import Any, Callable, Coroutine, Dict, List from typing import Any, Callable, Coroutine, Dict, List
@@ -50,6 +51,15 @@ class PluginWrapper:
# TODO enable this after websocket release # TODO enable this after websocket release
self.legacy_method_warning = False self.legacy_method_warning = False
home = get_homebrew_path()
mkdir_as_user(path.join(home, "settings", self.plugin_directory))
# TODO maybe dont chown this?
mkdir_as_user(path.join(home, "data"))
mkdir_as_user(path.join(home, "data", self.plugin_directory))
# TODO maybe dont chown this?
mkdir_as_user(path.join(home, "logs"))
mkdir_as_user(path.join(home, "logs", self.plugin_directory))
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name

View File

@@ -60,14 +60,8 @@ class SandboxedPlugin:
environ["DECKY_USER_HOME"] = helpers.get_home_path() environ["DECKY_USER_HOME"] = helpers.get_home_path()
environ["DECKY_HOME"] = helpers.get_homebrew_path() environ["DECKY_HOME"] = helpers.get_homebrew_path()
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory) environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "settings"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory) environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "data"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory) environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "logs"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory) environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
environ["DECKY_PLUGIN_NAME"] = self.name environ["DECKY_PLUGIN_NAME"] = self.name
if self.version: if self.version:

View File

@@ -7,10 +7,11 @@
"build": "rollup -c", "build": "rollup -c",
"watch": "rollup -c -w", "watch": "rollup -c -w",
"lint": "prettier -c src", "lint": "prettier -c src",
"typecheck": "tsc --noEmit",
"format": "prettier -c src -w" "format": "prettier -c src -w"
}, },
"devDependencies": { "devDependencies": {
"@decky/api": "^1.0.3", "@decky/api": "^1.0.4",
"@rollup/plugin-commonjs": "^21.1.0", "@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-image": "^3.0.3", "@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",

View File

@@ -35,8 +35,8 @@ dependencies:
devDependencies: devDependencies:
'@decky/api': '@decky/api':
specifier: ^1.0.3 specifier: ^1.0.4
version: 1.0.3 version: 1.0.4
'@rollup/plugin-commonjs': '@rollup/plugin-commonjs':
specifier: ^21.1.0 specifier: ^21.1.0
version: 21.1.0(rollup@2.79.1) version: 21.1.0(rollup@2.79.1)
@@ -318,8 +318,8 @@ packages:
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true dev: true
/@decky/api@1.0.3: /@decky/api@1.0.4:
resolution: {integrity: sha512-7hMKEHWcyz/bttx7DcKXqsOXcrtmC4CB6UwxRVrtlb/aolQtv1NVKHIEkIM6ND5hqTUU/VJ2HPUmCOwKm3Of0Q==} resolution: {integrity: sha512-YChHjlk//lOiIM2tlNSd6Qk9aduFJOtG+uRv1JaTzLewPRj4dDeupC+mbJJfarMGYa4nsLnJ6BsubTqboeb+VQ==}
dev: true dev: true
/@esbuild/aix-ppc64@0.20.2: /@esbuild/aix-ppc64@0.20.2:
@@ -1142,7 +1142,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001621 caniuse-lite: 1.0.30001623
electron-to-chromium: 1.4.783 electron-to-chromium: 1.4.783
node-releases: 2.0.14 node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0) update-browserslist-db: 1.0.16(browserslist@4.23.0)
@@ -1190,8 +1190,8 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/caniuse-lite@1.0.30001621: /caniuse-lite@1.0.30001623:
resolution: {integrity: sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==} resolution: {integrity: sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==}
dev: true dev: true
/ccount@2.0.1: /ccount@2.0.1:
@@ -1816,6 +1816,7 @@ packages:
/glob@7.2.3: /glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
inflight: 1.0.6 inflight: 1.0.6
@@ -3174,6 +3175,7 @@ packages:
/rimraf@2.7.1: /rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
@@ -3181,6 +3183,7 @@ packages:
/rimraf@3.0.2: /rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3

View File

@@ -1,17 +1,14 @@
import { sleep } from '@decky/ui'; import { sleep } from '@decky/ui';
import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react'; import { FunctionComponent, useEffect, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin'; import { uninstallPlugin } from '../plugin';
import { doRestart, doShutdown } from '../updater'; import { VerInfo, doRestart, doShutdown } from '../updater';
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
interface ReactErrorInfo {
error: Error;
info: ErrorInfo;
}
interface DeckyErrorBoundaryProps { interface DeckyErrorBoundaryProps {
error: ReactErrorInfo; error: ValveReactErrorInfo;
errorKey: string; errorKey: string;
identifier: string;
reset: () => void; reset: () => void;
} }
@@ -21,32 +18,6 @@ declare global {
} }
} }
const pluginErrorRegex = /\(http:\/\/localhost:1337\/plugins\/(.*)\//;
const pluginSourceMapErrorRegex = /\(decky:\/\/decky\/plugin\/(.*)\//;
const legacyPluginErrorRegex = /\(decky:\/\/decky\/legacy_plugin\/(.*)\/index.js/;
function getLikelyErrorSource(error: ReactErrorInfo): [source: string, wasPlugin: boolean] {
const pluginMatch = error.error.stack?.match(pluginErrorRegex);
if (pluginMatch) {
return [decodeURIComponent(pluginMatch[1]), true];
}
const pluginMatchViaMap = error.error.stack?.match(pluginSourceMapErrorRegex);
if (pluginMatchViaMap) {
return [decodeURIComponent(pluginMatchViaMap[1]), true];
}
const legacyPluginMatch = error.error.stack?.match(legacyPluginErrorRegex);
if (legacyPluginMatch) {
return [decodeURIComponent(legacyPluginMatch[1]), true];
}
if (error.error.stack?.includes('http://localhost:1337/')) {
return ['the Decky frontend', false];
}
return ['Steam', false];
}
export const startSSH = DeckyBackend.callable('utilities/start_ssh'); export const startSSH = DeckyBackend.callable('utilities/start_ssh');
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging'); export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
@@ -55,146 +26,171 @@ function ipToString(ip: number) {
} }
// Intentionally not localized since we can't really trust React here // Intentionally not localized since we can't really trust React here
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, reset }) => { const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, identifier, reset }) => {
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), ''); const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true); const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
const [debugAllowed, setDebugAllowed] = useState<boolean>(true); const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
const [errorSource, wasCausedByPlugin] = getLikelyErrorSource(error); // Intentionally doesn't use DeckyState.
const [versionInfo, setVersionInfo] = useState<VerInfo>();
const [errorSource, wasCausedByPlugin] = getLikelyErrorSourceFromValveReactError(error);
useEffect(() => {
DeckyPluginLoader.updateVersion().then(setVersionInfo);
}, []);
return ( return (
<div <>
style={{ <style>
overflow: 'scroll', {`
marginLeft: '15px', *:has(> .deckyErrorBoundary) {
color: 'white', overflow: scroll !important;
fontSize: '16px', }
userSelect: 'auto', `}
backgroundColor: 'black', </style>
marginTop: '48px', // Incase this is a page <div
}}
>
<h1
style={{ style={{
fontSize: '20px', overflow: 'auto',
display: 'inline-block', marginLeft: '15px',
marginTop: '15px', color: 'white',
fontSize: '16px',
userSelect: 'auto', userSelect: 'auto',
backgroundColor: 'black',
marginTop: '48px', // Incase this is a page
}} }}
className="deckyErrorBoundary"
> >
An error occured rendering this content. <h1
</h1> style={{
<p>This error likely occured in {getLikelyErrorSource(error)}.</p> fontSize: '20px',
{actionLog?.length > 0 && ( display: 'inline-block',
<pre> userSelect: 'auto',
}}
>
An error occured rendering this content.
</h1>
<pre style={{}}>
<code> <code>
Running actions... {identifier && `Error Reference: ${identifier}`}
{actionLog} {versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
</code> </code>
</pre> </pre>
)} <p>This error likely occured in {errorSource}.</p>
{actionsEnabled && ( {actionLog?.length > 0 && (
<> <pre>
<h3>Actions: </h3> <code>
<p>Use the touch screen.</p> Running actions...
<div style={{ display: 'block', marginBottom: '5px' }}> {actionLog}
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}> </code>
Retry </pre>
</button> )}
<button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}> {actionsEnabled && (
Restart Steam <>
</button> <h3>Actions: </h3>
</div> <p>Use the touch screen.</p>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Reloading UI...');
}}
>
Restart Decky
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Stopping Decky...');
doShutdown();
await sleep(5000);
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart();
}}
>
Disable Decky until next boot
</button>
</div>
{debugAllowed && (
<div style={{ display: 'block', marginBottom: '5px' }}> <div style={{ display: 'block', marginBottom: '5px' }}>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
Retry
</button>
<button <button
style={{ marginRight: '5px', padding: '5px' }} style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={() => {
setDebugAllowed(false); addLogLine('Restarting Steam...');
addLogLine('Enabling CEF debugger forwarding...'); SteamClient.User.StartRestart();
await starrCEFForwarding();
addLogLine('Enabling SSH...');
await startSSH();
addLogLine('Ready for debugging!');
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
addLogLine(`CEF Debugger: http://${ip}:8081`);
addLogLine(`SSH: deck@${ip}`);
}
}} }}
> >
Allow remote debugging and SSH until next boot Restart Steam
</button> </button>
</div> </div>
)}
{wasCausedByPlugin && (
<div style={{ display: 'block', marginBottom: '5px' }}> <div style={{ display: 'block', marginBottom: '5px' }}>
{'\n'}
<button <button
style={{ marginRight: '5px', padding: '5px' }} style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={async () => {
setActionsEnabled(false); setActionsEnabled(false);
addLogLine(`Uninstalling ${errorSource}...`);
await uninstallPlugin(errorSource);
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000);
addLogLine('Restarting Decky...'); addLogLine('Restarting Decky...');
doRestart(); doRestart();
await sleep(2000); await sleep(2000);
addLogLine('Reloading UI...');
}}
>
Restart Decky
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Stopping Decky...');
doShutdown();
await sleep(5000);
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart(); SteamClient.User.StartRestart();
}} }}
> >
Uninstall {errorSource} and restart Decky Disable Decky until next boot
</button> </button>
</div> </div>
)} {debugAllowed && (
</> <div style={{ display: 'block', marginBottom: '5px' }}>
)} <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setDebugAllowed(false);
addLogLine('Enabling CEF debugger forwarding...');
await starrCEFForwarding();
addLogLine('Enabling SSH...');
await startSSH();
addLogLine('Ready for debugging!');
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
addLogLine(`CEF Debugger: http://${ip}:8081`);
addLogLine(`SSH: deck@${ip}`);
}
}}
>
Allow remote debugging and SSH until next boot
</button>
</div>
)}
{wasCausedByPlugin && (
<div style={{ display: 'block', marginBottom: '5px' }}>
{'\n'}
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine(`Uninstalling ${errorSource}...`);
await uninstallPlugin(errorSource);
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart();
}}
>
Uninstall {errorSource} and restart Decky
</button>
</div>
)}
</>
)}
<pre <pre
style={{ style={{
marginTop: '15px', marginTop: '15px',
opacity: 0.7, opacity: 0.7,
userSelect: 'auto', userSelect: 'auto',
}} }}
> >
<code> <code>
{error.error.stack} {error.error.stack}
{'\n\n'} {'\n\n'}
Component Stack: Component Stack:
{error.info.componentStack} {error.info.componentStack}
</code> </code>
</pre> </pre>
</div> </div>
</>
); );
}; };

View File

@@ -1,5 +1,5 @@
import type { ToastData } from '@decky/api'; import type { ToastData } from '@decky/api';
import { FC, createContext, useContext, useEffect, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyToasterState { interface PublicDeckyToasterState {
toasts: Set<ToastData>; toasts: Set<ToastData>;
@@ -41,6 +41,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
interface Props { interface Props {
deckyToasterState: DeckyToasterState; deckyToasterState: DeckyToasterState;
children: ReactNode;
} }
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => { export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {

View File

@@ -1,5 +1,5 @@
import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { VFC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa'; import { FaEyeSlash } from 'react-icons/fa';
@@ -9,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState'; import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: VFC = () => { const PluginView: FC = () => {
const { hiddenPlugins } = useDeckyState(); const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();

View File

@@ -1,10 +1,10 @@
import { FC, createContext, useContext, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false); const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => { export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
const initial = tab.initialVisibility; const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial); const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this // HACK but i can't think of a better way to do this

View File

@@ -1,5 +1,5 @@
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui'; import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
import { CSSProperties, VFC } from 'react'; import { CSSProperties, FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BsGearFill } from 'react-icons/bs'; import { BsGearFill } from 'react-icons/bs';
import { FaArrowLeft, FaStore } from 'react-icons/fa'; import { FaArrowLeft, FaStore } from 'react-icons/fa';
@@ -14,7 +14,7 @@ const titleStyles: CSSProperties = {
top: '0px', top: '0px',
}; };
const TitleView: VFC = () => { const TitleView: FC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState(); const { activePlugin, closeActivePlugin } = useDeckyState();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -2,6 +2,7 @@ import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
import DeckyErrorBoundary from './components/DeckyErrorBoundary'; import DeckyErrorBoundary from './components/DeckyErrorBoundary';
import Logger from './logger'; import Logger from './logger';
import { getLikelyErrorSourceFromValveError } from './utils/errors';
declare global { declare global {
interface Window { interface Window {
@@ -11,6 +12,7 @@ declare global {
class ErrorBoundaryHook extends Logger { class ErrorBoundaryHook extends Logger {
private errorBoundaryPatch?: Patch; private errorBoundaryPatch?: Patch;
private errorCheckPatch?: Patch;
constructor() { constructor() {
super('ErrorBoundaryHook'); super('ErrorBoundaryHook');
@@ -35,13 +37,29 @@ class ErrorBoundaryHook extends Logger {
const errorReportingStore = initErrorReportingStore(); const errorReportingStore = initErrorReportingStore();
// NUH UH. // NUH UH.
Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', { // Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
get: () => false, // get: () => false,
}); // });
errorReportingStore.m_bEnabled = false; // errorReportingStore.m_bEnabled = false;
// @ts-ignore // @ts-ignore
// window.errorStore = errorReportingStore; window.errorStore = errorReportingStore;
const react15069WorkaroundRegex = / at .+\.componentDidCatch\..+\.callback /;
this.errorCheckPatch = replacePatch(Object.getPrototypeOf(errorReportingStore), 'BIsBlacklisted', (args: any[]) => {
const [errorSource, wasPlugin, shouldReport] = getLikelyErrorSourceFromValveError(args[0]);
this.debug('Caught an error', args, { errorSource, wasPlugin, shouldReport });
// react#15069 workaround. this took 2 hours to figure out.
if (
args[0]?.message?.[3]?.[0] &&
args[0]?.message?.[1]?.[0] == ' at console.error ' &&
react15069WorkaroundRegex.test(args[0].message[3][0])
) {
this.debug('ignoring early report caused by react#15069');
return true;
}
return shouldReport ? callOriginal : true;
});
const ValveErrorBoundary = findModuleExport( const ValveErrorBoundary = findModuleExport(
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch, (e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
@@ -53,8 +71,14 @@ class ErrorBoundaryHook extends Logger {
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) { this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
if (this.state.error) { if (this.state.error) {
const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore;
return ( return (
<DeckyErrorBoundary error={this.state.error} errorKey={this.state.errorKey} reset={() => this.Reset()} /> <DeckyErrorBoundary
error={this.state.error}
errorKey={this.props.errorKey}
identifier={`${store.product}_${store.version}_${this.state.identifierHash}`}
reset={() => this.Reset()}
/>
); );
} }
return callOriginal; return callOriginal;
@@ -62,6 +86,7 @@ class ErrorBoundaryHook extends Logger {
} }
deinit() { deinit() {
this.errorCheckPatch?.unpatch();
this.errorBoundaryPatch?.unpatch(); this.errorBoundaryPatch?.unpatch();
} }
} }

View File

@@ -0,0 +1,48 @@
import { ErrorInfo } from 'react';
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//;
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
export interface ValveReactErrorInfo {
error: Error;
info: ErrorInfo;
}
export interface ValveError {
identifier: string;
identifierHash: string;
message: string | [func: string, src: string, line: number, column: number];
}
export type ErrorSource = [source: string, wasPlugin: boolean, shouldReportToValve: boolean];
export function getLikelyErrorSourceFromValveError(error: ValveError): ErrorSource {
return getLikelyErrorSource(JSON.stringify(error?.message));
}
export function getLikelyErrorSourceFromValveReactError(error: ValveReactErrorInfo): ErrorSource {
return getLikelyErrorSource(error?.error?.stack);
}
export function getLikelyErrorSource(error?: string): ErrorSource {
const pluginMatch = error?.match(pluginErrorRegex);
if (pluginMatch) {
return [decodeURIComponent(pluginMatch[1]), true, false];
}
const pluginMatchViaMap = error?.match(pluginSourceMapErrorRegex);
if (pluginMatchViaMap) {
return [decodeURIComponent(pluginMatchViaMap[1]), true, false];
}
const legacyPluginMatch = error?.match(legacyPluginErrorRegex);
if (legacyPluginMatch) {
return [decodeURIComponent(legacyPluginMatch[1]), true, false];
}
if (error?.includes('http://localhost:1337/')) {
return ['the Decky frontend', false, false];
}
return ['Steam', false, true];
}