summaryrefslogtreecommitdiff
path: root/frontend/src/components/DeckyErrorBoundary.tsx
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-05-25 19:14:54 -0400
committerAAGaming <aagaming@riseup.net>2024-05-25 19:14:54 -0400
commita84a13c76d99f1e6f4505d43108a4111749e5035 (patch)
treee2826700cd371e6590818047551028d8179389bf /frontend/src/components/DeckyErrorBoundary.tsx
parent96cc72f2ca25ccb312b68a29aca755bb7df660ed (diff)
downloaddecky-loader-a84a13c76d99f1e6f4505d43108a4111749e5035.tar.gz
decky-loader-a84a13c76d99f1e6f4505d43108a4111749e5035.zip
Custom error handler and some misc fixes
Diffstat (limited to 'frontend/src/components/DeckyErrorBoundary.tsx')
-rw-r--r--frontend/src/components/DeckyErrorBoundary.tsx201
1 files changed, 201 insertions, 0 deletions
diff --git a/frontend/src/components/DeckyErrorBoundary.tsx b/frontend/src/components/DeckyErrorBoundary.tsx
new file mode 100644
index 00000000..a851b2e1
--- /dev/null
+++ b/frontend/src/components/DeckyErrorBoundary.tsx
@@ -0,0 +1,201 @@
+import { sleep } from '@decky/ui';
+import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react';
+
+import { uninstallPlugin } from '../plugin';
+import { doRestart, doShutdown } from '../updater';
+
+interface ReactErrorInfo {
+ error: Error;
+ info: ErrorInfo;
+}
+
+interface DeckyErrorBoundaryProps {
+ error: ReactErrorInfo;
+ errorKey: string;
+ reset: () => void;
+}
+
+declare global {
+ interface Window {
+ SystemNetworkStore?: any;
+ }
+}
+
+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');
+
+function ipToString(ip: number) {
+ return [(ip >>> 24) & 255, (ip >>> 16) & 255, (ip >>> 8) & 255, (ip >>> 0) & 255].join('.');
+}
+
+// Intentionally not localized since we can't really trust React here
+const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, 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);
+
+ return (
+ <div
+ style={{
+ overflow: 'scroll',
+ marginLeft: '15px',
+ color: 'white',
+ fontSize: '16px',
+ userSelect: 'auto',
+ backgroundColor: 'black',
+ marginTop: '48px', // Incase this is a page
+ }}
+ >
+ <h1
+ style={{
+ fontSize: '20px',
+ display: 'inline-block',
+ marginTop: '15px',
+ userSelect: 'auto',
+ }}
+ >
+ ⚠️ An error occured rendering this content.
+ </h1>
+ <p>This error likely occured in {getLikelyErrorSource(error)}.</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={() => 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 && (
+ <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>
+ );
+};
+
+export default DeckyErrorBoundary;