summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-08-07 16:14:18 -0400
committerAAGaming <aagaming@riseup.net>2024-08-07 16:14:18 -0400
commit65b6883dcc42944607eb0efa1f28e41f57335313 (patch)
tree38db3185d6720552daa978149279928d271df19a /frontend/src
parent166c7ea8a7ea74d9a61d84ebe16556cec9e7cc83 (diff)
downloaddecky-loader-65b6883dcc42944607eb0efa1f28e41f57335313.tar.gz
decky-loader-65b6883dcc42944607eb0efa1f28e41f57335313.zip
handle crashloops and disable decky for the user
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/DeckyErrorBoundary.tsx6
-rw-r--r--frontend/src/fallback.ts128
-rw-r--r--frontend/src/plugin-loader.tsx31
-rw-r--r--frontend/src/toaster.tsx19
4 files changed, 177 insertions, 7 deletions
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<DeckyErrorBoundaryProps> = ({ 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<DeckyErrorBoundaryProps> = ({ 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<DeckyErrorBoundaryProps> = ({ 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 = `
+ <svg class="fallbackDeckyIcon" xmlns="http://www.w3.org/2000/svg" height="100%" width="100%" viewBox="0 0 512 456">
+ <g>
+ <path
+ style="fill: none;"
+ d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
+ c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
+ c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
+ c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
+ C226.38,87.12,191.11,72.51,154.33,72.51z"
+ />
+
+ <ellipse
+ transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
+ style="fill: none;"
+ cx="154.33"
+ cy="211.33"
+ rx="69.33"
+ ry="69.33"
+ />
+ <path style="fill: none;" d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
+ <path
+ style="fill: currentColor;"
+ d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
+ C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
+ c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
+ c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
+ c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
+ c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
+ c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
+ c7.18,0,13,5.82,13,13V271z"
+ />
+ </g>
+ </svg>
+ `;
+ // #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 = `
+ <style>${fallbackCSS}</style>
+ ${fallbackIcon}
+ <span class="fallbackText">
+ <b>A crash loop has been detected and Decky has been disabled for this boot.</b>
+ <br>
+ <i>Steam will restart in 10 seconds...</i>
+ </span>
+ `;
+
+ 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 {
</PanelSectionRow>
<PanelSectionRow>
<pre style={{ overflowX: 'scroll' }}>
- <code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
+ <code>{e instanceof Error ? '' + e.stack : JSON.stringify(e)}</code>
</pre>
</PanelSectionRow>
<PanelSectionRow>
@@ -474,6 +497,12 @@ class PluginLoader extends Logger {
icon: <FaExclamationCircle />,
});
}
+
+ 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<void>((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) => (
- <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
+ <ErrorBoundary>
+ <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
+ </ErrorBoundary>
));
}
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;
}