mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Custom error handler and some misc fixes
This commit is contained in:
Vendored
+1
-1
@@ -41,7 +41,7 @@
|
||||
"deploy"
|
||||
],
|
||||
"detail": "Check for local runs, create a plugins folder",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --user --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip --root / && python -m pip install --user --break-system-packages --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ from .enums import UserType
|
||||
from logging import getLogger
|
||||
from packaging.version import Version
|
||||
|
||||
SSHD_UNIT = "sshd.service"
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
# global vars
|
||||
|
||||
@@ -160,7 +160,7 @@ class Loader:
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
if not batch:
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name, plugin.version))
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name, plugin.version, plugin.load_type))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
@@ -158,6 +158,7 @@ async def service_start(service_name : str) -> bool:
|
||||
|
||||
async def restart_webhelper() -> bool:
|
||||
logger.info("Restarting steamwebhelper")
|
||||
# TODO move to pkill
|
||||
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
|
||||
@@ -99,8 +99,10 @@ class PluginWrapper:
|
||||
return self
|
||||
|
||||
def stop(self, uninstall: bool = False):
|
||||
self._listener_task.cancel()
|
||||
if hasattr(self, "_listener_task"):
|
||||
self._listener_task.cancel()
|
||||
async def _(self: PluginWrapper):
|
||||
await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
|
||||
await self._socket.close_socket_connection()
|
||||
if hasattr(self, "_socket"):
|
||||
await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
|
||||
await self._socket.close_socket_connection()
|
||||
create_task(_(self))
|
||||
@@ -142,7 +142,10 @@ class SandboxedPlugin:
|
||||
try:
|
||||
self.log.info("Attempting to uninstall with plugin " + self.name + "'s \"_uninstall\" function.\n")
|
||||
if hasattr(self.Plugin, "_uninstall"):
|
||||
await self.Plugin._uninstall(self.Plugin)
|
||||
if self.api_version > 0:
|
||||
await self.Plugin._uninstall()
|
||||
else:
|
||||
await self.Plugin._uninstall(self.Plugin)
|
||||
self.log.info("Uninstalled " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_uninstall\" in " + self.name + "'s main.py" + "\n")
|
||||
|
||||
@@ -6,7 +6,7 @@ from os import getcwd, path, remove
|
||||
from typing import TYPE_CHECKING, List, TypedDict
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .localplatform.localplatform import chmod, service_restart, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
|
||||
from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
|
||||
import shutil
|
||||
from typing import List, TYPE_CHECKING, TypedDict
|
||||
import zipfile
|
||||
@@ -53,6 +53,7 @@ class Updater:
|
||||
context.ws.add_route("updater/get_version_info", self.get_version_info);
|
||||
context.ws.add_route("updater/check_for_updates", self.check_for_updates);
|
||||
context.ws.add_route("updater/do_restart", self.do_restart);
|
||||
context.ws.add_route("updater/do_shutdown", self.do_shutdown);
|
||||
context.ws.add_route("updater/do_update", self.do_update);
|
||||
context.ws.add_route("updater/get_testing_versions", self.get_testing_versions);
|
||||
context.ws.add_route("updater/download_testing_version", self.download_testing_version);
|
||||
@@ -184,8 +185,8 @@ class Updater:
|
||||
|
||||
logger.info("Updated loader installation.")
|
||||
await self.context.ws.emit("updater/finish_download")
|
||||
await self.do_restart()
|
||||
await tab.close_websocket()
|
||||
await self.do_restart()
|
||||
|
||||
async def do_update(self):
|
||||
logger.debug("Starting update.")
|
||||
@@ -242,6 +243,9 @@ class Updater:
|
||||
async def do_restart(self):
|
||||
await service_restart("plugin_loader")
|
||||
|
||||
async def do_shutdown(self):
|
||||
await service_stop("plugin_loader")
|
||||
|
||||
async def get_testing_versions(self) -> List[TestingVersion]:
|
||||
result: List[TestingVersion] = []
|
||||
async with ClientSession() as web:
|
||||
|
||||
@@ -79,6 +79,8 @@ class Utilities:
|
||||
context.ws.add_route("utilities/remove_css_from_tab", self.remove_css_from_tab)
|
||||
context.ws.add_route("utilities/allow_remote_debugging", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/disallow_remote_debugging", self.disallow_remote_debugging)
|
||||
context.ws.add_route("utilities/start_ssh", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/stop_ssh", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/filepicker_ls", self.filepicker_ls)
|
||||
context.ws.add_route("utilities/disable_rdt", self.disable_rdt)
|
||||
context.ws.add_route("utilities/enable_rdt", self.enable_rdt)
|
||||
@@ -284,6 +286,14 @@ class Utilities:
|
||||
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def start_ssh(self):
|
||||
await service_start(helpers.SSHD_UNIT)
|
||||
return True
|
||||
|
||||
async def stop_ssh(self):
|
||||
await service_stop(helpers.SSHD_UNIT)
|
||||
return True
|
||||
|
||||
async def filepicker_ls(self,
|
||||
path: str | None = None,
|
||||
include_files: bool = True,
|
||||
|
||||
+16
-15
@@ -10,30 +10,31 @@
|
||||
"format": "prettier -c src -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@decky/api": "^1.0.3",
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-image": "^3.0.2",
|
||||
"@rollup/plugin-image": "^3.0.3",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.5.0",
|
||||
"@types/react": "16.14.0",
|
||||
"@types/react-file-icon": "^1.0.1",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-file-icon": "^1.0.4",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@types/webpack": "^5.28.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"husky": "^8.0.3",
|
||||
"i18next-parser": "^8.0.0",
|
||||
"i18next-parser": "^8.13.0",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"inquirer": "^8.2.5",
|
||||
"inquirer": "^8.2.6",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"tslib": "^2.5.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"importSort": {
|
||||
@@ -44,12 +45,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "3.25.0",
|
||||
"filesize": "^10.0.7",
|
||||
"i18next": "^23.2.1",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"react-file-icon": "^1.3.0",
|
||||
"filesize": "^10.1.2",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"react-file-icon": "^1.4.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark-gfm": "^3.0.1"
|
||||
}
|
||||
|
||||
Generated
+739
-1003
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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<string, FC>;
|
||||
@@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone
|
||||
|
||||
interface Props {
|
||||
deckyGlobalComponentsState: DeckyGlobalComponentsState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
|
||||
|
||||
@@ -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<Props> = ({ children, deckyRouterState }) => {
|
||||
|
||||
@@ -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<Props> = ({ children, deckyState }) => {
|
||||
|
||||
@@ -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<DeckyToasterProps> = () => {
|
||||
}
|
||||
useEffect(() => {
|
||||
// not actually node but TS is shit
|
||||
let interval: NodeJS.Timer | null;
|
||||
let interval: NodeJS.Timeout | number | null;
|
||||
if (renderedToast) {
|
||||
interval = setTimeout(
|
||||
() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -59,7 +59,7 @@ const DropdownMultiselect: FC<{
|
||||
const [itemsSelected, setItemsSelected] = useState<any>(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),
|
||||
);
|
||||
|
||||
@@ -60,10 +60,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
strTitle={
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="title"
|
||||
i18n_args={{ artifact: artifact }}
|
||||
install_type={installType}
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="title"
|
||||
i18nArgs={{ artifact: artifact }}
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -71,17 +71,17 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
loading ? (
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="button_processing"
|
||||
install_type={installType}
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="button_processing"
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="button_idle"
|
||||
install_type={installType}
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="button_idle"
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -89,13 +89,13 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
>
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="desc"
|
||||
i18n_args={{
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="desc"
|
||||
i18nArgs={{
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
}}
|
||||
install_type={installType}
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
{loading && (
|
||||
|
||||
@@ -40,11 +40,11 @@ export async function setShowValveInternal(show: boolean) {
|
||||
export async function setShouldConnectToReactDevTools(enable: boolean) {
|
||||
DeckyPluginLoader.toaster.toast({
|
||||
title: enable ? (
|
||||
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'enabling'} />
|
||||
<TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'enabling'} />
|
||||
) : (
|
||||
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'disabling'} />
|
||||
<TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'disabling'} />
|
||||
),
|
||||
body: <TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'5secreload'} />,
|
||||
body: <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'5secreload'} />,
|
||||
icon: <FaReact />,
|
||||
});
|
||||
await sleep(5000);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
|
||||
|
||||
import DeckyErrorBoundary from './components/DeckyErrorBoundary';
|
||||
import Logger from './logger';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__ERRORBOUNDARY_HOOK_INSTANCE: any;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorBoundaryHook extends Logger {
|
||||
private errorBoundaryPatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super('ErrorBoundaryHook');
|
||||
|
||||
this.log('Initialized');
|
||||
window.__ERRORBOUNDARY_HOOK_INSTANCE?.deinit?.();
|
||||
window.__ERRORBOUNDARY_HOOK_INSTANCE = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
// valve writes only the sanest of code
|
||||
const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/;
|
||||
const initErrorReportingStore = findModuleExport(
|
||||
(e) => typeof e == 'function' && e?.toString && exp.test(e.toString()),
|
||||
);
|
||||
|
||||
if (!initErrorReportingStore) {
|
||||
this.error('could not find initErrorReportingStore! error boundary hook disabled!');
|
||||
return;
|
||||
}
|
||||
// will replace the existing one for us seemingly? doesnt matter anyway lol
|
||||
const errorReportingStore = initErrorReportingStore();
|
||||
|
||||
// NUH UH.
|
||||
Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
|
||||
get: () => false,
|
||||
});
|
||||
errorReportingStore.m_bEnabled = false;
|
||||
|
||||
// @ts-ignore
|
||||
// window.errorStore = errorReportingStore;
|
||||
|
||||
const ValveErrorBoundary = findModuleExport(
|
||||
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
|
||||
);
|
||||
if (!ValveErrorBoundary) {
|
||||
this.error('could not find ValveErrorBoundary');
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<DeckyErrorBoundary error={this.state.error} errorKey={this.state.errorKey} reset={() => this.Reset()} />
|
||||
);
|
||||
}
|
||||
return callOriginal;
|
||||
});
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.errorBoundaryPatch?.unpatch();
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundaryHook;
|
||||
@@ -21,6 +21,7 @@ import PluginUninstallModal from './components/modals/PluginUninstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import ErrorBoundaryHook from './errorboundary-hook';
|
||||
import { FrozenPluginService } from './frozen-plugins-service';
|
||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
@@ -61,6 +62,7 @@ const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: stri
|
||||
|
||||
class PluginLoader extends Logger {
|
||||
private plugins: Plugin[] = [];
|
||||
private errorBoundaryHook: ErrorBoundaryHook = new ErrorBoundaryHook();
|
||||
private tabsHook: TabsHook = new TabsHook();
|
||||
private routerHook: RouterHook = new RouterHook();
|
||||
public toaster: Toaster = new Toaster();
|
||||
@@ -79,6 +81,8 @@ class PluginLoader extends Logger {
|
||||
constructor() {
|
||||
super(PluginLoader.name);
|
||||
|
||||
this.errorBoundaryHook.init();
|
||||
|
||||
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
|
||||
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
|
||||
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
|
||||
@@ -185,12 +189,12 @@ class PluginLoader extends Logger {
|
||||
this.deckyState.setHasLoaderUpdate(true);
|
||||
if (this.notificationService.shouldNotify('deckyUpdates')) {
|
||||
this.toaster.toast({
|
||||
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
|
||||
title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />,
|
||||
body: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="decky_update_available"
|
||||
i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="decky_update_available"
|
||||
i18nArgs={{ tag_name: versionInfo?.remote?.tag_name }}
|
||||
/>
|
||||
),
|
||||
onClick: () => Router.Navigate('/decky/settings'),
|
||||
@@ -213,12 +217,12 @@ class PluginLoader extends Logger {
|
||||
const updates = await this.checkPluginUpdates();
|
||||
if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
|
||||
this.toaster.toast({
|
||||
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
|
||||
title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />,
|
||||
body: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_update"
|
||||
i18n_args={{ count: updates.size }}
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="plugin_update"
|
||||
i18nArgs={{ count: updates.size }}
|
||||
/>
|
||||
),
|
||||
onClick: () => Router.Navigate('/decky/settings/plugins'),
|
||||
@@ -294,6 +298,10 @@ class PluginLoader extends Logger {
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
deinitSteamFixes();
|
||||
deinitFilepickerPatches();
|
||||
this.routerHook.deinit();
|
||||
this.tabsHook.deinit();
|
||||
this.toaster.deinit();
|
||||
this.errorBoundaryHook.deinit();
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
@@ -365,7 +373,9 @@ class PluginLoader extends Logger {
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
let plugin_export: (serverAPI: any) => Plugin = await eval(await res.text());
|
||||
let plugin_export: (serverAPI: any) => Plugin = await eval(
|
||||
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
|
||||
);
|
||||
let plugin = plugin_export(this.createLegacyPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
@@ -384,7 +394,7 @@ class PluginLoader extends Logger {
|
||||
<PanelSection>
|
||||
<PanelSectionRow>
|
||||
<div className={quickAccessMenuClasses.FriendsTitle} style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
|
||||
<TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="error" />
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
@@ -395,9 +405,9 @@ class PluginLoader extends Logger {
|
||||
<PanelSectionRow>
|
||||
<div className={quickAccessMenuClasses.Text}>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_error_uninstall"
|
||||
i18n_args={{ name: name }}
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="plugin_error_uninstall"
|
||||
i18nArgs={{ name: name }}
|
||||
/>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
@@ -412,9 +422,9 @@ class PluginLoader extends Logger {
|
||||
this.toaster.toast({
|
||||
title: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_load_error.toast"
|
||||
i18n_args={{ name: name }}
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="plugin_load_error.toast"
|
||||
i18nArgs={{ name: name }}
|
||||
/>
|
||||
),
|
||||
body: '' + e,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// import reloadFix from './reload';
|
||||
// import restartFix from './restart';
|
||||
import restartFix from './restart';
|
||||
let fixes: Function[] = [];
|
||||
|
||||
export function deinitSteamFixes() {
|
||||
@@ -8,5 +8,5 @@ export function deinitSteamFixes() {
|
||||
|
||||
export async function initSteamFixes() {
|
||||
// fixes.push(await reloadFix());
|
||||
// fixes.push(await restartFix());
|
||||
fixes.push(await restartFix());
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { getFocusNavController, sleep } from '@decky/ui';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('ReloadSteamFix');
|
||||
|
||||
declare global {
|
||||
var GamepadNavTree: any;
|
||||
}
|
||||
|
||||
export default async function reloadFix() {
|
||||
// Hack to unbreak the ui when reloading it
|
||||
await sleep(4000);
|
||||
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
|
||||
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
|
||||
logger.log('Applied UI reload fix.');
|
||||
}
|
||||
|
||||
// This steamfix does not need to deinit.
|
||||
return () => {};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ToastData } from '@decky/api';
|
||||
import {
|
||||
Export,
|
||||
Patch,
|
||||
ToastData,
|
||||
afterPatch,
|
||||
findClass,
|
||||
findInReactTree,
|
||||
@@ -124,12 +124,12 @@ class Toaster extends Logger {
|
||||
this.node.alternate.type = this.node.type;
|
||||
}
|
||||
};
|
||||
const oRender = this.rNode.stateNode.__proto__.render;
|
||||
let int: NodeJS.Timer | undefined;
|
||||
const oRender = Object.getPrototypeOf(this.rNode.stateNode).render;
|
||||
let int: NodeJS.Timeout | undefined;
|
||||
this.rNode.stateNode.render = (...args: any[]) => {
|
||||
const ret = oRender.call(this.rNode.stateNode, ...args);
|
||||
if (ret && !this?.node?.return?.return) {
|
||||
clearInterval(int);
|
||||
int && clearInterval(int);
|
||||
int = setInterval(() => {
|
||||
const n = findToasterRoot(tree, 0);
|
||||
if (n?.return) {
|
||||
|
||||
@@ -25,9 +25,6 @@ export interface VerInfo {
|
||||
|
||||
export const doUpdate = DeckyBackend.callable('updater/do_update');
|
||||
export const doRestart = DeckyBackend.callable('updater/do_restart');
|
||||
export const doShutdown = DeckyBackend.callable('updater/do_shutdown');
|
||||
export const getVersionInfo = DeckyBackend.callable<[], VerInfo>('updater/get_version_info');
|
||||
export const checkForUpdates = DeckyBackend.callable<[], VerInfo>('updater/check_for_updates');
|
||||
|
||||
DeckyBackend.addEventListener('updater/finish_download', async () => {
|
||||
await doRestart();
|
||||
});
|
||||
|
||||
@@ -11,47 +11,42 @@ export enum TranslationClass {
|
||||
}
|
||||
|
||||
interface TranslationHelperProps {
|
||||
trans_class: TranslationClass;
|
||||
trans_text: string;
|
||||
i18n_args?: {};
|
||||
install_type?: number;
|
||||
transClass: TranslationClass;
|
||||
transText: string;
|
||||
i18nArgs?: {};
|
||||
installType?: number;
|
||||
}
|
||||
|
||||
const logger = new Logger('TranslationHelper');
|
||||
|
||||
const TranslationHelper: FC<TranslationHelperProps> = ({
|
||||
trans_class,
|
||||
trans_text,
|
||||
i18n_args = null,
|
||||
install_type = 0,
|
||||
}) => {
|
||||
const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null, installType = 0 }) => {
|
||||
return (
|
||||
<Translation>
|
||||
{(t, {}) => {
|
||||
switch (trans_class) {
|
||||
switch (transClass) {
|
||||
case TranslationClass.PLUGIN_LOADER:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_LOADER + '.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_LOADER + '.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_LOADER + '.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_LOADER + '.' + transText);
|
||||
case TranslationClass.PLUGIN_INSTALL_MODAL:
|
||||
switch (install_type) {
|
||||
switch (installType) {
|
||||
case InstallType.INSTALL:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText);
|
||||
case InstallType.REINSTALL:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText);
|
||||
case InstallType.UPDATE:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText);
|
||||
}
|
||||
case TranslationClass.DEVELOPER:
|
||||
return i18n_args
|
||||
? t(TranslationClass.DEVELOPER + '.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.DEVELOPER + '.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs)
|
||||
: t(TranslationClass.DEVELOPER + '.' + transText);
|
||||
default:
|
||||
logger.error('We should never fall in the default case!');
|
||||
return '';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! /usr/bin/env bash
|
||||
#!/usr/bin/env bash
|
||||
# Usage: deckdebug.sh DECKIP:8081
|
||||
# Dependencies: websocat jq curl chromium
|
||||
|
||||
|
||||
Reference in New Issue
Block a user