From 65b6883dcc42944607eb0efa1f28e41f57335313 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 7 Aug 2024 16:14:18 -0400 Subject: handle crashloops and disable decky for the user --- frontend/src/components/DeckyErrorBoundary.tsx | 6 +- frontend/src/fallback.ts | 128 +++++++++++++++++++++++++ frontend/src/plugin-loader.tsx | 31 +++++- frontend/src/toaster.tsx | 19 +++- 4 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 frontend/src/fallback.ts (limited to 'frontend/src') diff --git a/frontend/src/components/DeckyErrorBoundary.tsx b/frontend/src/components/DeckyErrorBoundary.tsx index 152e0f09..654db8a0 100644 --- a/frontend/src/components/DeckyErrorBoundary.tsx +++ b/frontend/src/components/DeckyErrorBoundary.tsx @@ -94,7 +94,7 @@ const DeckyErrorBoundary: FunctionComponent = ({ error, style={{ marginRight: '5px', padding: '5px' }} onClick={() => { addLogLine('Restarting Steam...'); - SteamClient.User.StartRestart(); + SteamClient.User.StartRestart(false); }} > Restart Steam @@ -121,7 +121,7 @@ const DeckyErrorBoundary: FunctionComponent = ({ error, doShutdown(); await sleep(5000); addLogLine('Restarting Steam...'); - SteamClient.User.StartRestart(); + SteamClient.User.StartRestart(false); }} > Disable Decky until next boot @@ -166,7 +166,7 @@ const DeckyErrorBoundary: FunctionComponent = ({ error, await sleep(2000); addLogLine('Restarting Steam...'); await sleep(500); - SteamClient.User.StartRestart(); + SteamClient.User.StartRestart(false); }} > Uninstall {errorSource} and restart Decky diff --git a/frontend/src/fallback.ts b/frontend/src/fallback.ts new file mode 100644 index 00000000..fc39b272 --- /dev/null +++ b/frontend/src/fallback.ts @@ -0,0 +1,128 @@ +// THIS FILE MUST BE ENTIRELY SELF-CONTAINED! DO NOT USE PACKAGES! +interface Window { + FocusNavController: any; + GamepadNavTree: any; + deckyFallbackLoaded?: boolean; +} + +(async () => { + try { + if (window.deckyFallbackLoaded) return; + window.deckyFallbackLoaded = true; + + // #region utils + function sleep(ms: number) { + return new Promise((res) => setTimeout(res, ms)); + } + // #endregion + + // #region DeckyIcon + const fallbackIcon = ` + + + + + + + + + + `; + // #endregion + + // #region findSP + // from @decky/ui + function getFocusNavController(): any { + return window.GamepadNavTree?.m_context?.m_controller || window.FocusNavController; + } + + function getGamepadNavigationTrees(): any { + const focusNav = getFocusNavController(); + const context = focusNav.m_ActiveContext || focusNav.m_LastActiveContext; + return context?.m_rgGamepadNavigationTrees; + } + + function findSP(): Window { + // old (SP as host) + if (document.title == 'SP') return window; + // new (SP as popup) + const navTrees = getGamepadNavigationTrees(); + return navTrees?.find((x: any) => x.m_ID == 'root_1_').Root.Element.ownerDocument.defaultView; + } + // #endregion + + const fallbackCSS = ` + .fallbackContainer { + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + flex-direction: column; + z-index: 99999999; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + backdrop-filter: blur(8px) brightness(40%); + } + .fallbackDeckyIcon { + width: 96px; + height: 96px; + padding-bottom: 1rem; + } + `; + + const fallbackHTML = ` + + ${fallbackIcon} + + A crash loop has been detected and Decky has been disabled for this boot. +
+ Steam will restart in 10 seconds... +
+ `; + + await sleep(4000); + + const win = findSP() || window; + + const container = Object.assign(document.createElement('div'), { + innerHTML: fallbackHTML, + }); + container.classList.add('fallbackContainer'); + + win.document.body.appendChild(container); + + await sleep(10000); + + SteamClient.User.StartShutdown(false); + } catch (e) { + console.error('Error showing fallback!', e); + } +})(); diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index f03877fa..4e0a01e9 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -172,11 +172,32 @@ class PluginLoader extends Logger { .then(() => this.log('Initialized')); } + private checkForSP(): boolean { + try { + return !!findSP(); + } catch (e) { + this.warn('Error checking for SP tab', e); + return false; + } + } + + private async runCrashChecker() { + const spExists = this.checkForSP(); + await sleep(5000); + if (spExists && !this.checkForSP()) { + // SP died after plugin loaded. Give up and let the loader's crash loop detection handle it. + this.error('SP died during startup. Restarting webhelper.'); + await this.restartWebhelper(); + } + } + private getPluginsFromBackend = DeckyBackend.callable< [], { name: string; version: string; load_type: PluginLoadType }[] >('loader/get_plugins'); + private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper'); + private async loadPlugins() { let registration: any; const uiMode = await new Promise( @@ -192,6 +213,7 @@ class PluginLoader extends Logger { await sleep(100); } } + this.runCrashChecker(); const plugins = await this.getPluginsFromBackend(); const pluginLoadPromises = []; const loadStart = performance.now(); @@ -395,6 +417,7 @@ class PluginLoader extends Logger { version?: string, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, ) { + let spExists = this.checkForSP(); try { switch (loadType) { case PluginLoadType.ESMODULE_V1: @@ -442,7 +465,7 @@ class PluginLoader extends Logger {
-              {e instanceof Error ? e.stack : JSON.stringify(e)}
+              {e instanceof Error ? '' + e.stack : JSON.stringify(e)}
             
@@ -474,6 +497,12 @@ class PluginLoader extends Logger { icon: , }); } + + if (spExists && !this.checkForSP()) { + // SP died after plugin loaded. Give up and let the loader's crash loop detection handle it. + this.error('SP died after loading plugin. Restarting webhelper.'); + await this.restartWebhelper(); + } } async callServerMethod(methodName: string, args = {}) { diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx index e45b14a4..4f67c589 100644 --- a/frontend/src/toaster.tsx +++ b/frontend/src/toaster.tsx @@ -1,5 +1,13 @@ import type { ToastData, ToastNotification } from '@decky/api'; -import { Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui'; +import { + ErrorBoundary, + Patch, + callOriginal, + findModuleExport, + injectFCTrampoline, + replacePatch, + sleep, +} from '@decky/ui'; import Toast from './components/Toast'; import Logger from './logger'; @@ -21,6 +29,8 @@ declare global { class Toaster extends Logger { private toastPatch?: Patch; + private markReady!: () => void; + private ready = new Promise((r) => (this.markReady = r)); constructor() { super('Toaster'); @@ -34,13 +44,16 @@ class Toaster extends Logger { this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => { if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) { return args[0].group.notifications.map((notification: any) => ( - + + + )); } return callOriginal; }); this.log('Initialized'); + sleep(4000).then(this.markReady); } toast(toast: ToastData): ToastNotification { @@ -107,7 +120,7 @@ class Toaster extends Logger { } }, toast.expiration); } - window.NotificationStore.ProcessNotification(info, toastData, ToastType.New); + this.ready.then(() => window.NotificationStore.ProcessNotification(info, toastData, ToastType.New)); return toastResult; } -- cgit v1.2.3