mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 00:37:49 +00:00
error boundary now properly reports steam errors
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
import { sleep } from '@decky/ui';
|
||||
import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react';
|
||||
import { FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||
|
||||
import { uninstallPlugin } from '../plugin';
|
||||
import { doRestart, doShutdown } from '../updater';
|
||||
|
||||
interface ReactErrorInfo {
|
||||
error: Error;
|
||||
info: ErrorInfo;
|
||||
}
|
||||
import { VerInfo, doRestart, doShutdown } from '../updater';
|
||||
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
|
||||
|
||||
interface DeckyErrorBoundaryProps {
|
||||
error: ReactErrorInfo;
|
||||
error: ValveReactErrorInfo;
|
||||
errorKey: string;
|
||||
identifier: string;
|
||||
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 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
|
||||
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 [actionsEnabled, setActionsEnabled] = 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 (
|
||||
<div
|
||||
style={{
|
||||
overflow: 'scroll',
|
||||
marginLeft: '15px',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
userSelect: 'auto',
|
||||
backgroundColor: 'black',
|
||||
marginTop: '48px', // Incase this is a page
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
*:has(> .deckyErrorBoundary) {
|
||||
overflow: scroll !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
display: 'inline-block',
|
||||
marginTop: '15px',
|
||||
overflow: 'auto',
|
||||
marginLeft: '15px',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
userSelect: 'auto',
|
||||
backgroundColor: 'black',
|
||||
marginTop: '48px', // Incase this is a page
|
||||
}}
|
||||
className="deckyErrorBoundary"
|
||||
>
|
||||
⚠️ An error occured rendering this content.
|
||||
</h1>
|
||||
<p>This error likely occured in {getLikelyErrorSource(error)}.</p>
|
||||
{actionLog?.length > 0 && (
|
||||
<pre>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
display: 'inline-block',
|
||||
userSelect: 'auto',
|
||||
}}
|
||||
>
|
||||
⚠️ An error occured rendering this content.
|
||||
</h1>
|
||||
<pre style={{}}>
|
||||
<code>
|
||||
Running actions...
|
||||
{actionLog}
|
||||
{identifier && `Error Reference: ${identifier}`}
|
||||
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<>
|
||||
<h3>Actions: </h3>
|
||||
<p>Use the touch screen.</p>
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
|
||||
Retry
|
||||
</button>
|
||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}>
|
||||
Restart Steam
|
||||
</button>
|
||||
</div>
|
||||
<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 && (
|
||||
<p>This error likely occured in {errorSource}.</p>
|
||||
{actionLog?.length > 0 && (
|
||||
<pre>
|
||||
<code>
|
||||
Running actions...
|
||||
{actionLog}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<>
|
||||
<h3>Actions: </h3>
|
||||
<p>Use the touch screen.</p>
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
|
||||
Retry
|
||||
</button>
|
||||
<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}`);
|
||||
}
|
||||
onClick={() => {
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart();
|
||||
}}
|
||||
>
|
||||
Allow remote debugging and SSH until next boot
|
||||
Restart Steam
|
||||
</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('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...');
|
||||
await sleep(500);
|
||||
SteamClient.User.StartRestart();
|
||||
}}
|
||||
>
|
||||
Uninstall {errorSource} and restart Decky
|
||||
Disable Decky until next boot
|
||||
</button>
|
||||
</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
|
||||
style={{
|
||||
marginTop: '15px',
|
||||
opacity: 0.7,
|
||||
userSelect: 'auto',
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{error.error.stack}
|
||||
{'\n\n'}
|
||||
Component Stack:
|
||||
{error.info.componentStack}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<pre
|
||||
style={{
|
||||
marginTop: '15px',
|
||||
opacity: 0.7,
|
||||
userSelect: 'auto',
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{error.error.stack}
|
||||
{'\n\n'}
|
||||
Component Stack:
|
||||
{error.info.componentStack}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
toasts: Set<ToastData>;
|
||||
@@ -41,6 +41,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
|
||||
|
||||
interface Props {
|
||||
deckyToasterState: DeckyToasterState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
@@ -9,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
|
||||
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||
import TitleView from './TitleView';
|
||||
|
||||
const PluginView: VFC = () => {
|
||||
const PluginView: FC = () => {
|
||||
const { hiddenPlugins } = useDeckyState();
|
||||
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, createContext, useContext, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useState } from 'react';
|
||||
|
||||
const QuickAccessVisibleState = createContext<boolean>(false);
|
||||
|
||||
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 [visible, setVisible] = useState<boolean>(initial);
|
||||
// HACK but i can't think of a better way to do this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
|
||||
import { CSSProperties, VFC } from 'react';
|
||||
import { CSSProperties, FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsGearFill } from 'react-icons/bs';
|
||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||
@@ -14,7 +14,7 @@ const titleStyles: CSSProperties = {
|
||||
top: '0px',
|
||||
};
|
||||
|
||||
const TitleView: VFC = () => {
|
||||
const TitleView: FC = () => {
|
||||
const { activePlugin, closeActivePlugin } = useDeckyState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user