mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-15 18:13:40 +03:00
error boundary now properly reports steam errors
This commit is contained in:
2
.github/workflows/typecheck.yml
vendored
2
.github/workflows/typecheck.yml
vendored
@@ -41,4 +41,4 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tsc (TypeScript)
|
- name: Run tsc (TypeScript)
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: $(pnpm bin)/tsc --noEmit
|
run: pnpm run typecheck
|
||||||
@@ -8,6 +8,7 @@ from .sandboxed_plugin import SandboxedPlugin
|
|||||||
from .messages import MethodCallRequest, SocketMessageType
|
from .messages import MethodCallRequest, SocketMessageType
|
||||||
from ..enums import PluginLoadType
|
from ..enums import PluginLoadType
|
||||||
from ..localplatform.localsocket import LocalSocket
|
from ..localplatform.localsocket import LocalSocket
|
||||||
|
from ..helpers import get_homebrew_path, mkdir_as_user
|
||||||
|
|
||||||
from typing import Any, Callable, Coroutine, Dict, List
|
from typing import Any, Callable, Coroutine, Dict, List
|
||||||
|
|
||||||
@@ -50,6 +51,15 @@ class PluginWrapper:
|
|||||||
# TODO enable this after websocket release
|
# TODO enable this after websocket release
|
||||||
self.legacy_method_warning = False
|
self.legacy_method_warning = False
|
||||||
|
|
||||||
|
home = get_homebrew_path()
|
||||||
|
mkdir_as_user(path.join(home, "settings", self.plugin_directory))
|
||||||
|
# TODO maybe dont chown this?
|
||||||
|
mkdir_as_user(path.join(home, "data"))
|
||||||
|
mkdir_as_user(path.join(home, "data", self.plugin_directory))
|
||||||
|
# TODO maybe dont chown this?
|
||||||
|
mkdir_as_user(path.join(home, "logs"))
|
||||||
|
mkdir_as_user(path.join(home, "logs", self.plugin_directory))
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|||||||
@@ -60,14 +60,8 @@ class SandboxedPlugin:
|
|||||||
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
||||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "settings"))
|
|
||||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
|
||||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
||||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "data"))
|
|
||||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
|
||||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
||||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "logs"))
|
|
||||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
|
||||||
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
||||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||||
if self.version:
|
if self.version:
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"watch": "rollup -c -w",
|
"watch": "rollup -c -w",
|
||||||
"lint": "prettier -c src",
|
"lint": "prettier -c src",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"format": "prettier -c src -w"
|
"format": "prettier -c src -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@decky/api": "^1.0.3",
|
"@decky/api": "^1.0.4",
|
||||||
"@rollup/plugin-commonjs": "^21.1.0",
|
"@rollup/plugin-commonjs": "^21.1.0",
|
||||||
"@rollup/plugin-image": "^3.0.3",
|
"@rollup/plugin-image": "^3.0.3",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
|||||||
17
frontend/pnpm-lock.yaml
generated
17
frontend/pnpm-lock.yaml
generated
@@ -35,8 +35,8 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@decky/api':
|
'@decky/api':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.4
|
||||||
version: 1.0.3
|
version: 1.0.4
|
||||||
'@rollup/plugin-commonjs':
|
'@rollup/plugin-commonjs':
|
||||||
specifier: ^21.1.0
|
specifier: ^21.1.0
|
||||||
version: 21.1.0(rollup@2.79.1)
|
version: 21.1.0(rollup@2.79.1)
|
||||||
@@ -318,8 +318,8 @@ packages:
|
|||||||
to-fast-properties: 2.0.0
|
to-fast-properties: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@decky/api@1.0.3:
|
/@decky/api@1.0.4:
|
||||||
resolution: {integrity: sha512-7hMKEHWcyz/bttx7DcKXqsOXcrtmC4CB6UwxRVrtlb/aolQtv1NVKHIEkIM6ND5hqTUU/VJ2HPUmCOwKm3Of0Q==}
|
resolution: {integrity: sha512-YChHjlk//lOiIM2tlNSd6Qk9aduFJOtG+uRv1JaTzLewPRj4dDeupC+mbJJfarMGYa4nsLnJ6BsubTqboeb+VQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@esbuild/aix-ppc64@0.20.2:
|
/@esbuild/aix-ppc64@0.20.2:
|
||||||
@@ -1142,7 +1142,7 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001621
|
caniuse-lite: 1.0.30001623
|
||||||
electron-to-chromium: 1.4.783
|
electron-to-chromium: 1.4.783
|
||||||
node-releases: 2.0.14
|
node-releases: 2.0.14
|
||||||
update-browserslist-db: 1.0.16(browserslist@4.23.0)
|
update-browserslist-db: 1.0.16(browserslist@4.23.0)
|
||||||
@@ -1190,8 +1190,8 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/caniuse-lite@1.0.30001621:
|
/caniuse-lite@1.0.30001623:
|
||||||
resolution: {integrity: sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==}
|
resolution: {integrity: sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ccount@2.0.1:
|
/ccount@2.0.1:
|
||||||
@@ -1816,6 +1816,7 @@ packages:
|
|||||||
|
|
||||||
/glob@7.2.3:
|
/glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
|
deprecated: Glob versions prior to v9 are no longer supported
|
||||||
dependencies:
|
dependencies:
|
||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
inflight: 1.0.6
|
inflight: 1.0.6
|
||||||
@@ -3174,6 +3175,7 @@ packages:
|
|||||||
|
|
||||||
/rimraf@2.7.1:
|
/rimraf@2.7.1:
|
||||||
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
|
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
|
||||||
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
@@ -3181,6 +3183,7 @@ packages:
|
|||||||
|
|
||||||
/rimraf@3.0.2:
|
/rimraf@3.0.2:
|
||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { sleep } from '@decky/ui';
|
import { sleep } from '@decky/ui';
|
||||||
import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react';
|
import { FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||||
|
|
||||||
import { uninstallPlugin } from '../plugin';
|
import { uninstallPlugin } from '../plugin';
|
||||||
import { doRestart, doShutdown } from '../updater';
|
import { VerInfo, doRestart, doShutdown } from '../updater';
|
||||||
|
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
|
||||||
interface ReactErrorInfo {
|
|
||||||
error: Error;
|
|
||||||
info: ErrorInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeckyErrorBoundaryProps {
|
interface DeckyErrorBoundaryProps {
|
||||||
error: ReactErrorInfo;
|
error: ValveReactErrorInfo;
|
||||||
errorKey: string;
|
errorKey: string;
|
||||||
|
identifier: string;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,32 +18,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 startSSH = DeckyBackend.callable('utilities/start_ssh');
|
||||||
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
|
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
|
||||||
|
|
||||||
@@ -55,146 +26,171 @@ function ipToString(ip: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Intentionally not localized since we can't really trust React here
|
// Intentionally not localized since we can't really trust React here
|
||||||
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, reset }) => {
|
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, identifier, reset }) => {
|
||||||
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
|
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
|
||||||
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
|
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
|
||||||
const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
|
const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
|
||||||
const [errorSource, wasCausedByPlugin] = getLikelyErrorSource(error);
|
// Intentionally doesn't use DeckyState.
|
||||||
|
const [versionInfo, setVersionInfo] = useState<VerInfo>();
|
||||||
|
const [errorSource, wasCausedByPlugin] = getLikelyErrorSourceFromValveReactError(error);
|
||||||
|
useEffect(() => {
|
||||||
|
DeckyPluginLoader.updateVersion().then(setVersionInfo);
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={{
|
<style>
|
||||||
overflow: 'scroll',
|
{`
|
||||||
marginLeft: '15px',
|
*:has(> .deckyErrorBoundary) {
|
||||||
color: 'white',
|
overflow: scroll !important;
|
||||||
fontSize: '16px',
|
}
|
||||||
userSelect: 'auto',
|
`}
|
||||||
backgroundColor: 'black',
|
</style>
|
||||||
marginTop: '48px', // Incase this is a page
|
<div
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
style={{
|
style={{
|
||||||
fontSize: '20px',
|
overflow: 'auto',
|
||||||
display: 'inline-block',
|
marginLeft: '15px',
|
||||||
marginTop: '15px',
|
color: 'white',
|
||||||
|
fontSize: '16px',
|
||||||
userSelect: 'auto',
|
userSelect: 'auto',
|
||||||
|
backgroundColor: 'black',
|
||||||
|
marginTop: '48px', // Incase this is a page
|
||||||
}}
|
}}
|
||||||
|
className="deckyErrorBoundary"
|
||||||
>
|
>
|
||||||
⚠️ An error occured rendering this content.
|
<h1
|
||||||
</h1>
|
style={{
|
||||||
<p>This error likely occured in {getLikelyErrorSource(error)}.</p>
|
fontSize: '20px',
|
||||||
{actionLog?.length > 0 && (
|
display: 'inline-block',
|
||||||
<pre>
|
userSelect: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⚠️ An error occured rendering this content.
|
||||||
|
</h1>
|
||||||
|
<pre style={{}}>
|
||||||
<code>
|
<code>
|
||||||
Running actions...
|
{identifier && `Error Reference: ${identifier}`}
|
||||||
{actionLog}
|
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
<p>This error likely occured in {errorSource}.</p>
|
||||||
{actionsEnabled && (
|
{actionLog?.length > 0 && (
|
||||||
<>
|
<pre>
|
||||||
<h3>Actions: </h3>
|
<code>
|
||||||
<p>Use the touch screen.</p>
|
Running actions...
|
||||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
{actionLog}
|
||||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
|
</code>
|
||||||
Retry
|
</pre>
|
||||||
</button>
|
)}
|
||||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}>
|
{actionsEnabled && (
|
||||||
Restart Steam
|
<>
|
||||||
</button>
|
<h3>Actions: </h3>
|
||||||
</div>
|
<p>Use the touch screen.</p>
|
||||||
<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' }}>
|
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||||
|
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
style={{ marginRight: '5px', padding: '5px' }}
|
style={{ marginRight: '5px', padding: '5px' }}
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
setDebugAllowed(false);
|
addLogLine('Restarting Steam...');
|
||||||
addLogLine('Enabling CEF debugger forwarding...');
|
SteamClient.User.StartRestart();
|
||||||
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
|
Restart Steam
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{wasCausedByPlugin && (
|
|
||||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||||
{'\n'}
|
|
||||||
<button
|
<button
|
||||||
style={{ marginRight: '5px', padding: '5px' }}
|
style={{ marginRight: '5px', padding: '5px' }}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setActionsEnabled(false);
|
setActionsEnabled(false);
|
||||||
addLogLine(`Uninstalling ${errorSource}...`);
|
|
||||||
await uninstallPlugin(errorSource);
|
|
||||||
await DeckyPluginLoader.frozenPluginsService.invalidate();
|
|
||||||
await DeckyPluginLoader.hiddenPluginsService.invalidate();
|
|
||||||
await sleep(1000);
|
|
||||||
addLogLine('Restarting Decky...');
|
addLogLine('Restarting Decky...');
|
||||||
doRestart();
|
doRestart();
|
||||||
await sleep(2000);
|
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...');
|
addLogLine('Restarting Steam...');
|
||||||
await sleep(500);
|
|
||||||
SteamClient.User.StartRestart();
|
SteamClient.User.StartRestart();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Uninstall {errorSource} and restart Decky
|
Disable Decky until next boot
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<pre
|
||||||
style={{
|
style={{
|
||||||
marginTop: '15px',
|
marginTop: '15px',
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
userSelect: 'auto',
|
userSelect: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<code>
|
<code>
|
||||||
{error.error.stack}
|
{error.error.stack}
|
||||||
{'\n\n'}
|
{'\n\n'}
|
||||||
Component Stack:
|
Component Stack:
|
||||||
{error.info.componentStack}
|
{error.info.componentStack}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ToastData } from '@decky/api';
|
import type { ToastData } from '@decky/api';
|
||||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface PublicDeckyToasterState {
|
interface PublicDeckyToasterState {
|
||||||
toasts: Set<ToastData>;
|
toasts: Set<ToastData>;
|
||||||
@@ -41,6 +41,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
deckyToasterState: DeckyToasterState;
|
deckyToasterState: DeckyToasterState;
|
||||||
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
|
import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
|
||||||
import { VFC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaEyeSlash } from 'react-icons/fa';
|
import { FaEyeSlash } from 'react-icons/fa';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
|
|||||||
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||||
import TitleView from './TitleView';
|
import TitleView from './TitleView';
|
||||||
|
|
||||||
const PluginView: VFC = () => {
|
const PluginView: FC = () => {
|
||||||
const { hiddenPlugins } = useDeckyState();
|
const { hiddenPlugins } = useDeckyState();
|
||||||
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||||
const visible = useQuickAccessVisible();
|
const visible = useQuickAccessVisible();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FC, createContext, useContext, useState } from 'react';
|
import { FC, ReactNode, createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
const QuickAccessVisibleState = createContext<boolean>(false);
|
const QuickAccessVisibleState = createContext<boolean>(false);
|
||||||
|
|
||||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||||
|
|
||||||
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => {
|
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
|
||||||
const initial = tab.initialVisibility;
|
const initial = tab.initialVisibility;
|
||||||
const [visible, setVisible] = useState<boolean>(initial);
|
const [visible, setVisible] = useState<boolean>(initial);
|
||||||
// HACK but i can't think of a better way to do this
|
// HACK but i can't think of a better way to do this
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
|
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
|
||||||
import { CSSProperties, VFC } from 'react';
|
import { CSSProperties, FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BsGearFill } from 'react-icons/bs';
|
import { BsGearFill } from 'react-icons/bs';
|
||||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||||
@@ -14,7 +14,7 @@ const titleStyles: CSSProperties = {
|
|||||||
top: '0px',
|
top: '0px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TitleView: VFC = () => {
|
const TitleView: FC = () => {
|
||||||
const { activePlugin, closeActivePlugin } = useDeckyState();
|
const { activePlugin, closeActivePlugin } = useDeckyState();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
|
|||||||
|
|
||||||
import DeckyErrorBoundary from './components/DeckyErrorBoundary';
|
import DeckyErrorBoundary from './components/DeckyErrorBoundary';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
|
import { getLikelyErrorSourceFromValveError } from './utils/errors';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -11,6 +12,7 @@ declare global {
|
|||||||
|
|
||||||
class ErrorBoundaryHook extends Logger {
|
class ErrorBoundaryHook extends Logger {
|
||||||
private errorBoundaryPatch?: Patch;
|
private errorBoundaryPatch?: Patch;
|
||||||
|
private errorCheckPatch?: Patch;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('ErrorBoundaryHook');
|
super('ErrorBoundaryHook');
|
||||||
@@ -35,13 +37,29 @@ class ErrorBoundaryHook extends Logger {
|
|||||||
const errorReportingStore = initErrorReportingStore();
|
const errorReportingStore = initErrorReportingStore();
|
||||||
|
|
||||||
// NUH UH.
|
// NUH UH.
|
||||||
Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
|
// Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
|
||||||
get: () => false,
|
// get: () => false,
|
||||||
});
|
// });
|
||||||
errorReportingStore.m_bEnabled = false;
|
// errorReportingStore.m_bEnabled = false;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// window.errorStore = errorReportingStore;
|
window.errorStore = errorReportingStore;
|
||||||
|
|
||||||
|
const react15069WorkaroundRegex = / at .+\.componentDidCatch\..+\.callback /;
|
||||||
|
this.errorCheckPatch = replacePatch(Object.getPrototypeOf(errorReportingStore), 'BIsBlacklisted', (args: any[]) => {
|
||||||
|
const [errorSource, wasPlugin, shouldReport] = getLikelyErrorSourceFromValveError(args[0]);
|
||||||
|
this.debug('Caught an error', args, { errorSource, wasPlugin, shouldReport });
|
||||||
|
// react#15069 workaround. this took 2 hours to figure out.
|
||||||
|
if (
|
||||||
|
args[0]?.message?.[3]?.[0] &&
|
||||||
|
args[0]?.message?.[1]?.[0] == ' at console.error ' &&
|
||||||
|
react15069WorkaroundRegex.test(args[0].message[3][0])
|
||||||
|
) {
|
||||||
|
this.debug('ignoring early report caused by react#15069');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return shouldReport ? callOriginal : true;
|
||||||
|
});
|
||||||
|
|
||||||
const ValveErrorBoundary = findModuleExport(
|
const ValveErrorBoundary = findModuleExport(
|
||||||
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
|
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
|
||||||
@@ -53,8 +71,14 @@ class ErrorBoundaryHook extends Logger {
|
|||||||
|
|
||||||
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
|
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
|
const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore;
|
||||||
return (
|
return (
|
||||||
<DeckyErrorBoundary error={this.state.error} errorKey={this.state.errorKey} reset={() => this.Reset()} />
|
<DeckyErrorBoundary
|
||||||
|
error={this.state.error}
|
||||||
|
errorKey={this.props.errorKey}
|
||||||
|
identifier={`${store.product}_${store.version}_${this.state.identifierHash}`}
|
||||||
|
reset={() => this.Reset()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return callOriginal;
|
return callOriginal;
|
||||||
@@ -62,6 +86,7 @@ class ErrorBoundaryHook extends Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit() {
|
deinit() {
|
||||||
|
this.errorCheckPatch?.unpatch();
|
||||||
this.errorBoundaryPatch?.unpatch();
|
this.errorBoundaryPatch?.unpatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
frontend/src/utils/errors.ts
Normal file
48
frontend/src/utils/errors.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { ErrorInfo } from 'react';
|
||||||
|
|
||||||
|
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//;
|
||||||
|
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
|
||||||
|
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
|
||||||
|
|
||||||
|
export interface ValveReactErrorInfo {
|
||||||
|
error: Error;
|
||||||
|
info: ErrorInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValveError {
|
||||||
|
identifier: string;
|
||||||
|
identifierHash: string;
|
||||||
|
message: string | [func: string, src: string, line: number, column: number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ErrorSource = [source: string, wasPlugin: boolean, shouldReportToValve: boolean];
|
||||||
|
|
||||||
|
export function getLikelyErrorSourceFromValveError(error: ValveError): ErrorSource {
|
||||||
|
return getLikelyErrorSource(JSON.stringify(error?.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLikelyErrorSourceFromValveReactError(error: ValveReactErrorInfo): ErrorSource {
|
||||||
|
return getLikelyErrorSource(error?.error?.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLikelyErrorSource(error?: string): ErrorSource {
|
||||||
|
const pluginMatch = error?.match(pluginErrorRegex);
|
||||||
|
if (pluginMatch) {
|
||||||
|
return [decodeURIComponent(pluginMatch[1]), true, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginMatchViaMap = error?.match(pluginSourceMapErrorRegex);
|
||||||
|
if (pluginMatchViaMap) {
|
||||||
|
return [decodeURIComponent(pluginMatchViaMap[1]), true, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyPluginMatch = error?.match(legacyPluginErrorRegex);
|
||||||
|
if (legacyPluginMatch) {
|
||||||
|
return [decodeURIComponent(legacyPluginMatch[1]), true, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error?.includes('http://localhost:1337/')) {
|
||||||
|
return ['the Decky frontend', false, false];
|
||||||
|
}
|
||||||
|
return ['Steam', false, true];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user