From a84a13c76d99f1e6f4505d43108a4111749e5035 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sat, 25 May 2024 19:14:54 -0400 Subject: Custom error handler and some misc fixes --- frontend/src/components/DeckyErrorBoundary.tsx | 201 +++++++++++++++++++++ .../src/components/DeckyGlobalComponentsState.tsx | 3 +- frontend/src/components/DeckyRouterState.tsx | 3 +- frontend/src/components/DeckyState.tsx | 3 +- frontend/src/components/DeckyToaster.tsx | 5 +- frontend/src/components/DeckyToasterState.tsx | 2 +- frontend/src/components/Toast.tsx | 3 +- .../src/components/modals/DropdownMultiselect.tsx | 2 +- .../src/components/modals/PluginInstallModal.tsx | 28 +-- 9 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/DeckyErrorBoundary.tsx (limited to 'frontend/src/components') 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 = ({ error, reset }) => { + const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), ''); + const [actionsEnabled, setActionsEnabled] = useState(true); + const [debugAllowed, setDebugAllowed] = useState(true); + const [errorSource, wasCausedByPlugin] = getLikelyErrorSource(error); + + return ( +
+

+ ⚠️ An error occured rendering this content. +

+

This error likely occured in {getLikelyErrorSource(error)}.

+ {actionLog?.length > 0 && ( +
+          
+            Running actions...
+            {actionLog}
+          
+        
+ )} + {actionsEnabled && ( + <> +

Actions:

+

Use the touch screen.

+
+ + +
+
+ + +
+ {debugAllowed && ( +
+ +
+ )} + {wasCausedByPlugin && ( +
+ {'\n'} + +
+ )} + + )} + +
+        
+          {error.error.stack}
+          {'\n\n'}
+          Component Stack:
+          {error.info.componentStack}
+        
+      
+
+ ); +}; + +export default DeckyErrorBoundary; diff --git a/frontend/src/components/DeckyGlobalComponentsState.tsx b/frontend/src/components/DeckyGlobalComponentsState.tsx index fe45588b..475d1e4a 100644 --- a/frontend/src/components/DeckyGlobalComponentsState.tsx +++ b/frontend/src/components/DeckyGlobalComponentsState.tsx @@ -1,4 +1,4 @@ -import { FC, createContext, useContext, useEffect, useState } from 'react'; +import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; interface PublicDeckyGlobalComponentsState { components: Map; @@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone interface Props { deckyGlobalComponentsState: DeckyGlobalComponentsState; + children: ReactNode; } export const DeckyGlobalComponentsStateContextProvider: FC = ({ diff --git a/frontend/src/components/DeckyRouterState.tsx b/frontend/src/components/DeckyRouterState.tsx index 0c8bb1ba..426ed731 100644 --- a/frontend/src/components/DeckyRouterState.tsx +++ b/frontend/src/components/DeckyRouterState.tsx @@ -1,4 +1,4 @@ -import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react'; +import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import type { RouteProps } from 'react-router'; export interface RouterEntry { @@ -71,6 +71,7 @@ export const useDeckyRouterState = () => useContext(DeckyRouterStateContext); interface Props { deckyRouterState: DeckyRouterState; + children: ReactNode; } export const DeckyRouterStateContextProvider: FC = ({ children, deckyRouterState }) => { diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index 749e27ce..75106e62 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -1,4 +1,4 @@ -import { FC, createContext, useContext, useEffect, useState } from 'react'; +import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service'; import { Plugin } from '../plugin'; @@ -134,6 +134,7 @@ export const useDeckyState = () => useContext(DeckyStateContext); interface Props { deckyState: DeckyState; + children?: ReactNode; } export const DeckyStateContextProvider: FC = ({ children, deckyState }) => { diff --git a/frontend/src/components/DeckyToaster.tsx b/frontend/src/components/DeckyToaster.tsx index 00b7b4db..1cb51d65 100644 --- a/frontend/src/components/DeckyToaster.tsx +++ b/frontend/src/components/DeckyToaster.tsx @@ -1,4 +1,5 @@ -import { ToastData, joinClassNames } from '@decky/ui'; +import type { ToastData } from '@decky/api'; +import { joinClassNames } from '@decky/ui'; import { FC, useEffect, useState } from 'react'; import { ReactElement } from 'react-markdown/lib/react-markdown'; @@ -28,7 +29,7 @@ const DeckyToaster: FC = () => { } useEffect(() => { // not actually node but TS is shit - let interval: NodeJS.Timer | null; + let interval: NodeJS.Timeout | number | null; if (renderedToast) { interval = setTimeout( () => { diff --git a/frontend/src/components/DeckyToasterState.tsx b/frontend/src/components/DeckyToasterState.tsx index d6c3871f..8d0a5d45 100644 --- a/frontend/src/components/DeckyToasterState.tsx +++ b/frontend/src/components/DeckyToasterState.tsx @@ -1,4 +1,4 @@ -import { ToastData } from '@decky/ui'; +import type { ToastData } from '@decky/api'; import { FC, createContext, useContext, useEffect, useState } from 'react'; interface PublicDeckyToasterState { diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index ab01671a..79e3d864 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -1,4 +1,5 @@ -import { ToastData, findModule, joinClassNames } from '@decky/ui'; +import type { ToastData } from '@decky/api'; +import { findModule, joinClassNames } from '@decky/ui'; import { FunctionComponent } from 'react'; interface ToastProps { diff --git a/frontend/src/components/modals/DropdownMultiselect.tsx b/frontend/src/components/modals/DropdownMultiselect.tsx index 4c5cf7b1..255c6fa0 100644 --- a/frontend/src/components/modals/DropdownMultiselect.tsx +++ b/frontend/src/components/modals/DropdownMultiselect.tsx @@ -59,7 +59,7 @@ const DropdownMultiselect: FC<{ const [itemsSelected, setItemsSelected] = useState(selected); const { t } = useTranslation(); - const handleItemSelect = useCallback((checked, value) => { + const handleItemSelect = useCallback((checked: boolean, value: any) => { setItemsSelected((x: any) => checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value), ); diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index 8b3128a1..c6c90264 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -60,10 +60,10 @@ const PluginInstallModal: FC = ({ strTitle={
} @@ -71,17 +71,17 @@ const PluginInstallModal: FC = ({ loading ? (
) : (
) @@ -89,13 +89,13 @@ const PluginInstallModal: FC = ({ >
{loading && ( -- cgit v1.2.3