diff options
| author | AAGaming <aagaming@riseup.net> | 2024-05-25 19:14:54 -0400 |
|---|---|---|
| committer | AAGaming <aagaming@riseup.net> | 2024-05-25 19:14:54 -0400 |
| commit | a84a13c76d99f1e6f4505d43108a4111749e5035 (patch) | |
| tree | e2826700cd371e6590818047551028d8179389bf /frontend/src/components/DeckyErrorBoundary.tsx | |
| parent | 96cc72f2ca25ccb312b68a29aca755bb7df660ed (diff) | |
| download | decky-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.tsx | 201 |
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; |
