Compare commits

..

29 Commits

Author SHA1 Message Date
AAGaming b3a5f9659f fix router hook on stable 2024-08-03 18:16:51 -04:00
AAGaming d444248f62 hopefully fix bootloops 2024-08-03 14:07:12 -04:00
AAGaming 9570d0b0c2 python moment 2024-08-03 14:04:23 -04:00
AAGaming 8a90605a64 actually implement timeout in the http_request_legacy handler 2024-08-03 14:04:23 -04:00
AAGaming 9d0dbbb790 disable filepicker patches for now
we need to write one for  SteamClient.System.OpenFileDialog eventually
2024-08-03 14:04:23 -04:00
AAGaming 2303023ad4 chore: remove outdated TODO 2024-08-03 14:04:23 -04:00
AAGaming 599279c137 chore: lint 2024-08-03 14:04:23 -04:00
AAGaming 44be6ba3dd fix incorrect parameter order 2024-08-03 14:04:22 -04:00
AAGaming 5e270de990 fix game theme music error 2024-08-03 14:04:22 -04:00
AAGaming 52b40b7d0c wait for SP to exist before loading plugins when booted in gamepadui mode 2024-08-03 14:04:22 -04:00
AAGaming 3cb150b9fb wait for unlock before showing toasts 2024-08-03 14:04:22 -04:00
AAGaming bac1ef5f4d increase webhelper restart inject delay 2024-08-03 14:04:22 -04:00
AAGaming d9ab176bc7 increase the wait a bit 2024-08-03 14:04:22 -04:00
AAGaming ff856c7148 wait a bit before loading plugins to avoid race conditions 2024-08-03 14:04:21 -04:00
AAGaming 62293d2316 fix(webpack): wait for most chunks to load before starting 2024-08-03 14:04:21 -04:00
AAGaming e00d517119 fix(DeckyIcon): fix decky icon overflowing in toasts
could still use some padding but eh
2024-08-03 14:04:21 -04:00
AAGaming 241aec26e8 chore(toaster): remove log that gets spammed a lot 2024-08-03 14:04:21 -04:00
AAGaming 491d298fd7 chore(deps): bump @decky/api, @decky/ui 2024-08-03 14:04:21 -04:00
AAGaming 262b62ea4f chore(titleview): migrate to Navigation 2024-08-03 14:04:20 -04:00
AAGaming 4cde25c43e fix(tabshook): remove useless leftover alternate.type set in deinit 2024-08-03 14:04:20 -04:00
AAGaming b8bf9f343c refactor(router): add preliminary support for multiple router hook implementations 2024-08-03 14:04:20 -04:00
AAGaming 4cf80595ad feat(toaster):add support for dismissing toasts and new indicator 2024-08-03 14:04:20 -04:00
AAGaming 4c23549748 workaround python throw on webhelper restarts 2024-08-03 14:04:20 -04:00
AAGaming 7b21e81caa inject into desktop ui windows since they don't reload on mode change anymore anyway 2024-08-03 14:04:20 -04:00
AAGaming c2443ee2c5 fix(toaster): remove critical toast logic as ProcessNotification handles it for us 2024-08-03 14:04:19 -04:00
AAGaming df52ebe7ed feat(toaster): add support for fullTemplateTitle 2024-08-03 14:04:19 -04:00
AAGaming b93fc8b557 feat(toaster): render notifications in the quick access menu 2024-08-03 14:04:19 -04:00
AAGaming 88e7919a12 implement new toaster hook
this also supports the notification list and probably also desktop toasts (UI wip, read location enum prop from toast component probably)
2024-08-03 14:04:19 -04:00
AAGaming 28c7254ef6 initial implementation of new router and qam hooks 2024-08-03 14:04:19 -04:00
20 changed files with 83 additions and 392 deletions
+1 -1
View File
@@ -120,7 +120,7 @@
"dependsOn": [ "dependsOn": [
"checkforsettings" "checkforsettings"
], ],
"command": "ssh -t ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | poetry run sh -c \"cd ${config:deckdir}/homebrew/services; sudo -SE env \"PATH=\\$PATH\" python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'", "command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | poetry run sh -c \"cd ${config:deckdir}/homebrew/services; sudo -SE env \"PATH=\\$PATH\" python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'",
"problemMatcher": [] "problemMatcher": []
}, },
{ {
+1 -8
View File
@@ -52,9 +52,6 @@ async def csrf_middleware(request: Request, handler: Handler):
return await handler(request) return await handler(request)
return Response(text='Forbidden', status=403) return Response(text='Forbidden', status=403)
def create_inject_script(script: str) -> str:
return "try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{await import('http://localhost:1337/frontend/%s?v=%s')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}" % (script, get_loader_version(), )
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated # Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
def get_homebrew_path() -> str: def get_homebrew_path() -> str:
return localplatform.get_unprivileged_path() return localplatform.get_unprivileged_path()
@@ -96,11 +93,7 @@ def get_system_pythonpaths() -> list[str]:
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"], proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
# TODO make this less insane # TODO make this less insane
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage] capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage]
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
proc.check_returncode()
versions = [x.strip() for x in proc.stdout.decode().strip().split("\n")]
return [x for x in versions if x and not x.isspace()]
except Exception as e: except Exception as e:
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}") logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
return [] return []
+1 -24
View File
@@ -95,14 +95,12 @@
} }
}, },
"PluginListIndex": { "PluginListIndex": {
"freeze": "アップデートを凍結",
"hide": "クイックアクセス: 非表示", "hide": "クイックアクセス: 非表示",
"no_plugin": "プラグインがインストールされていません!", "no_plugin": "プラグインがインストールされていません!",
"plugin_actions": "プラグインアクション", "plugin_actions": "プラグインアクション",
"reinstall": "再インストール", "reinstall": "再インストール",
"reload": "再読み込み", "reload": "再読み込み",
"show": "クイックアクセス: 表示", "show": "クイックアクセス: 表示",
"unfreeze": "アップデートを許可",
"uninstall": "アンインストール", "uninstall": "アンインストール",
"update_all_other": "{{count}} 個のプラグインをアップデート", "update_all_other": "{{count}} 個のプラグインをアップデート",
"update_to": "{{name}} を更新" "update_to": "{{name}} を更新"
@@ -187,19 +185,9 @@
"SettingsIndex": { "SettingsIndex": {
"developer_title": "開発者", "developer_title": "開発者",
"general_title": "一般", "general_title": "一般",
"plugins_title": "プラグイン", "plugins_title": "プラグイン"
"testing_title": "テスト"
}, },
"Store": { "Store": {
"download_progress_info": {
"download_zip": "プラグインのダウンロード中",
"increment_count": "ダウンロード数の増加",
"installing_plugin": "プラグインのインストール中",
"open_zip": "zipファイルを展開中",
"parse_zip": "zipファイルの解析中",
"start": "初期化中",
"uninstalling_previous": "以前のコピーのアンインストール"
},
"store_contrib": { "store_contrib": {
"desc": "Decky Plugin Storeに貢献したい場合は、GitHubのSteamDeckHomebrew/decky-plugin-templateリポジトリを確認してください。 開発と配布に関する情報は README で入手できます。", "desc": "Decky Plugin Storeに貢献したい場合は、GitHubのSteamDeckHomebrew/decky-plugin-templateリポジトリを確認してください。 開発と配布に関する情報は README で入手できます。",
"label": "貢献" "label": "貢献"
@@ -223,10 +211,6 @@
"about": "概要", "about": "概要",
"alph_asce": "アルファベット(Z to A)", "alph_asce": "アルファベット(Z to A)",
"alph_desc": "アルファベット(A to Z)", "alph_desc": "アルファベット(A to Z)",
"date_asce": "古い順",
"date_desc": "新しい順",
"downloads_asce": "ダウンロード数が少ない順",
"downloads_desc": "ダウンロード数が多い順",
"title": "閲覧" "title": "閲覧"
}, },
"store_testing_cta": "Decky Loaderチームを支援するために、新しいプラグインのテストを検討してください!", "store_testing_cta": "Decky Loaderチームを支援するために、新しいプラグインのテストを検討してください!",
@@ -247,13 +231,6 @@
"testing": "テスト" "testing": "テスト"
} }
}, },
"Testing": {
"download": "ダウンロード",
"error": "PRのインストールエラー",
"header": "Decky Loaderの以下のバージョンは、公開されているサードパーティのPull Requestからビルドされたものです。 Decky Loaderチームはその機能や安全性を検証しておらず、内容も古い可能性があります。",
"loading": "Pull Requestの読み込み中...",
"start_download_toast": "PR #{{id}}のダウンロード中"
},
"TitleView": { "TitleView": {
"decky_store_desc": "Deckyストアを開く", "decky_store_desc": "Deckyストアを開く",
"settings_desc": "Decky設定を開く" "settings_desc": "Decky設定を開く"
@@ -37,9 +37,6 @@ def get_live_reload() -> bool:
def get_keep_systemd_service() -> bool: def get_keep_systemd_service() -> bool:
return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1" return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1"
def get_use_cef_close_workaround() -> bool:
return ON_LINUX and os.getenv("USE_CEF_CLOSE_WORKAROUND", "1") == "1"
def get_log_level() -> int: def get_log_level() -> int:
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[ return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
os.getenv("LOG_LEVEL", "INFO") os.getenv("LOG_LEVEL", "INFO")
@@ -1,5 +1,3 @@
from re import compile
from asyncio import Lock
import os, pwd, grp, sys, logging import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT from subprocess import call, run, DEVNULL, PIPE, STDOUT
from ..enums import UserType from ..enums import UserType
@@ -229,39 +227,3 @@ def get_unprivileged_user() -> str:
user = 'deck' user = 'deck'
return user return user
# Works around the CEF debugger TCP socket not closing properly when Steam restarts
# Group 1 is PID, group 2 is FD. this also filters for "steamwebhelper" in the process name.
cef_socket_lsof_regex = compile(r"^p(\d+)(?:\s|.)+csteamwebhelper(?:\s|.)+f(\d+)(?:\s|.)+TST=LISTEN")
close_cef_socket_lock = Lock()
async def close_cef_socket():
async with close_cef_socket_lock:
if _get_effective_user_id() != 0:
logger.warn("Can't close CEF socket as Decky isn't running as root.")
return
# Look for anything listening TCP on port 8080
lsof = run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], capture_output=True, text=True)
if lsof.returncode != 0 or len(lsof.stdout) < 1:
logger.error(f"lsof call failed in close_cef_socket! return code: {str(lsof.returncode)}")
return
lsof_data = cef_socket_lsof_regex.match(lsof.stdout)
if not lsof_data:
logger.error("lsof regex match failed in close_cef_socket!")
return
pid = lsof_data.group(1)
fd = lsof_data.group(2)
logger.info(f"Closing CEF socket with PID {pid} and FD {fd}")
# Use gdb to inject a close() call for the socket fd into steamwebhelper
gdb_ret = run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"], env={"LD_LIBRARY_PATH": ""})
if gdb_ret.returncode != 0:
logger.error(f"Failed to close CEF socket with gdb! return code: {str(gdb_ret.returncode)}", exc_info=True)
return
logger.info("CEF socket closed")
@@ -55,7 +55,4 @@ def get_unprivileged_user() -> str:
return os.getenv("UNPRIVILEGED_USER", os.getlogin()) return os.getenv("UNPRIVILEGED_USER", os.getlogin())
async def restart_webhelper() -> bool: async def restart_webhelper() -> bool:
return True # Stubbed return True # Stubbed
async def close_cef_socket():
return # Stubbed
+7 -31
View File
@@ -7,17 +7,14 @@ from .localplatform.localplatform import (chmod, chown, service_stop, service_st
get_privileged_path, restart_webhelper) get_privileged_path, restart_webhelper)
if hasattr(sys, '_MEIPASS'): if hasattr(sys, '_MEIPASS'):
chmod(sys._MEIPASS, 755) # type: ignore chmod(sys._MEIPASS, 755) # type: ignore
# Full imports # Full imports
import multiprocessing
multiprocessing.freeze_support()
from asyncio import AbstractEventLoop, CancelledError, Task, all_tasks, current_task, gather, new_event_loop, set_event_loop, sleep from asyncio import AbstractEventLoop, CancelledError, Task, all_tasks, current_task, gather, new_event_loop, set_event_loop, sleep
from logging import basicConfig, getLogger from logging import basicConfig, getLogger
from os import path from os import path
from traceback import format_exc from traceback import format_exc
from time import time import multiprocessing
import aiohttp_cors # pyright: ignore [reportMissingTypeStubs]
import aiohttp_cors # pyright: ignore [reportMissingTypeStubs]
# Partial imports # Partial imports
from aiohttp import client_exceptions from aiohttp import client_exceptions
from aiohttp.web import Application, Response, Request, get, run_app, static # pyright: ignore [reportUnknownVariableType] from aiohttp.web import Application, Response, Request, get, run_app, static # pyright: ignore [reportUnknownVariableType]
@@ -26,7 +23,7 @@ from setproctitle import getproctitle, setproctitle, setthreadtitle
# local modules # local modules
from .browser import PluginBrowser from .browser import PluginBrowser
from .helpers import (REMOTE_DEBUGGER_UNIT, create_inject_script, csrf_middleware, get_csrf_token, get_loader_version, from .helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, get_loader_version,
mkdir_as_user, get_system_pythonpaths, get_effective_user_id) mkdir_as_user, get_system_pythonpaths, get_effective_user_id)
from .injector import get_gamepadui_tab, Tab from .injector import get_gamepadui_tab, Tab
@@ -76,9 +73,6 @@ class PluginManager:
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings) self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
self.utilities = Utilities(self) self.utilities = Utilities(self)
self.updater = Updater(self) self.updater = Updater(self)
self.last_webhelper_exit: float = 0
self.webhelper_crash_count: int = 0
self.inject_fallback: bool = False
jinja_setup(self.web_app) jinja_setup(self.web_app)
@@ -100,21 +94,6 @@ class PluginManager:
self.cors.add(route) # pyright: ignore [reportUnknownMemberType] self.cors.add(route) # pyright: ignore [reportUnknownMemberType]
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))]) self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
async def handle_crash(self):
new_time = time()
if (new_time - self.last_webhelper_exit < 60):
self.webhelper_crash_count += 1
logger.warn(f"webhelper crashed within a minute from last crash! crash count: {self.webhelper_crash_count}")
else:
self.webhelper_crash_count = 0
self.last_webhelper_exit = new_time
# should never happen
if (self.webhelper_crash_count > 4):
await self.updater.do_shutdown()
# Give up
exit(0)
async def shutdown(self, _: Application): async def shutdown(self, _: Application):
try: try:
logger.info(f"Shutting down...") logger.info(f"Shutting down...")
@@ -206,7 +185,6 @@ class PluginManager:
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket. # If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321 # This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
logger.info("CEF has disconnected...") logger.info("CEF has disconnected...")
await self.handle_crash()
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready. # At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
except Exception: except Exception:
if not self.reinject: if not self.reinject:
@@ -214,7 +192,6 @@ class PluginManager:
logger.error("Exception while reading page events " + format_exc()) logger.error("Exception while reading page events " + format_exc())
await tab.close_websocket() await tab.close_websocket()
self.js_ctx_tab = None self.js_ctx_tab = None
await self.handle_crash()
pass pass
# while True: # while True:
# await sleep(5) # await sleep(5)
@@ -232,11 +209,7 @@ class PluginManager:
await restart_webhelper() await restart_webhelper()
await sleep(1) # To give CEF enough time to close down the websocket await sleep(1) # To give CEF enough time to close down the websocket
return # We'll catch the next tab in the main loop return # We'll catch the next tab in the main loop
await tab.evaluate_js(create_inject_script("index.js" if self.webhelper_crash_count < 3 else "fallback.js"), False, False, False) await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{await import('http://localhost:1337/frontend/index.js?v=%s')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}" % (get_loader_version(), ), False, False, False)
if self.webhelper_crash_count > 2:
self.reinject = False
await sleep(1)
await self.updater.do_shutdown()
except: except:
logger.info("Failed to inject JavaScript into tab\n" + format_exc()) logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass pass
@@ -251,6 +224,9 @@ def main():
# Fix windows/flask not recognising that .js means 'application/javascript' # Fix windows/flask not recognising that .js means 'application/javascript'
import mimetypes import mimetypes
mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('application/javascript', '.js')
# Required for multiprocessing support in frozen files
multiprocessing.freeze_support()
else: else:
if get_effective_user_id() != 0: if get_effective_user_id() != 0:
logger.warning(f"decky is running as an unprivileged user, this is not officially supported and may cause issues") logger.warning(f"decky is running as an unprivileged user, this is not officially supported and may cause issues")
@@ -1,9 +1,9 @@
import sys
from os import path, environ from os import path, environ
from signal import SIG_IGN, SIGINT, SIGTERM, getsignal, signal from signal import SIG_IGN, SIGINT, SIGTERM, getsignal, signal
from importlib.util import module_from_spec, spec_from_file_location from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, loads from json import dumps, loads
from logging import getLogger from logging import getLogger
from sys import exit, path as syspath, modules as sysmodules
from traceback import format_exc from traceback import format_exc
from asyncio import (get_event_loop, new_event_loop, from asyncio import (get_event_loop, new_event_loop,
set_event_loop) set_event_loop)
@@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
from ..enums import UserType from ..enums import UserType
from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport] from .. import helpers
from typing import List, TypeVar, Any from typing import List, TypeVar, Any
@@ -78,12 +78,12 @@ class SandboxedPlugin:
environ["DECKY_PLUGIN_AUTHOR"] = self.author environ["DECKY_PLUGIN_AUTHOR"] = self.author
# append the plugin's `py_modules` to the recognized python paths # append the plugin's `py_modules` to the recognized python paths
sys.path.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules")) syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
#TODO: FIX IN A LESS CURSED WAY #TODO: FIX IN A LESS CURSED WAY
keys = [key for key in sys.modules if key.startswith("decky_loader.")] keys = [key for key in sysmodules if key.startswith("decky_loader.")]
for key in keys: for key in keys:
sys.modules[key.replace("decky_loader.", "")] = sys.modules[key] sysmodules[key.replace("decky_loader.", "")] = sysmodules[key]
from .imports import decky from .imports import decky
async def emit(event: str, *args: Any) -> None: async def emit(event: str, *args: Any) -> None:
@@ -95,9 +95,9 @@ class SandboxedPlugin:
# copy the docstring over so we don't have to duplicate it # copy the docstring over so we don't have to duplicate it
emit.__doc__ = decky.emit.__doc__ emit.__doc__ = decky.emit.__doc__
decky.emit = emit decky.emit = emit
sys.modules["decky"] = decky sysmodules["decky"] = decky
# provided for compatibility # provided for compatibility
sys.modules["decky_plugin"] = decky sysmodules["decky_plugin"] = decky
spec = spec_from_file_location("_", self.file) spec = spec_from_file_location("_", self.file)
assert spec is not None assert spec is not None
@@ -123,7 +123,7 @@ class SandboxedPlugin:
get_event_loop().create_task(socket.setup_server()) get_event_loop().create_task(socket.setup_server())
except: except:
self.log.error("Failed to start " + self.name + "!\n" + format_exc()) self.log.error("Failed to start " + self.name + "!\n" + format_exc())
sys.exit(0) exit(0)
try: try:
get_event_loop().run_forever() get_event_loop().run_forever()
except SystemExit: except SystemExit:
@@ -180,7 +180,7 @@ class SandboxedPlugin:
loop = get_event_loop() loop = get_event_loop()
loop.call_soon_threadsafe(loop.stop) loop.call_soon_threadsafe(loop.stop)
sys.exit(0) exit(0)
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]} d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
try: try:
+2 -10
View File
@@ -20,8 +20,9 @@ from .browser import PluginInstallRequest, PluginInstallType
if TYPE_CHECKING: if TYPE_CHECKING:
from .main import PluginManager from .main import PluginManager
from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
from .localplatform.localplatform import ON_WINDOWS
from . import helpers from . import helpers
from .localplatform.localplatform import ON_WINDOWS, service_stop, service_start, get_home_path, get_username, get_use_cef_close_workaround, close_cef_socket, restart_webhelper from .localplatform.localplatform import service_stop, service_start, get_home_path, get_username
class FilePickerObj(TypedDict): class FilePickerObj(TypedDict):
file: Path file: Path
@@ -77,8 +78,6 @@ class Utilities:
context.ws.add_route("utilities/get_tab_id", self.get_tab_id) context.ws.add_route("utilities/get_tab_id", self.get_tab_id)
context.ws.add_route("utilities/get_user_info", self.get_user_info) context.ws.add_route("utilities/get_user_info", self.get_user_info)
context.ws.add_route("utilities/http_request", self.http_request_legacy) context.ws.add_route("utilities/http_request", self.http_request_legacy)
context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper)
context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.web_app.add_routes([ context.web_app.add_routes([
@@ -288,13 +287,6 @@ class Utilities:
await service_stop(helpers.SSHD_UNIT) await service_stop(helpers.SSHD_UNIT)
return True return True
async def close_cef_socket(self):
if get_use_cef_close_workaround():
await close_cef_socket()
async def restart_webhelper(self):
await restart_webhelper()
async def filepicker_ls(self, async def filepicker_ls(self,
path: str | None = None, path: str | None = None,
include_files: bool = True, include_files: bool = True,
-1
View File
@@ -1,6 +1,5 @@
[Unit] [Unit]
Description=SteamDeck Plugin Loader Description=SteamDeck Plugin Loader
After=network.target
[Service] [Service]
Type=simple Type=simple
User=root User=root
-1
View File
@@ -1,6 +1,5 @@
[Unit] [Unit]
Description=SteamDeck Plugin Loader Description=SteamDeck Plugin Loader
After=network.target
[Service] [Service]
Type=simple Type=simple
User=root User=root
+1 -1
View File
@@ -47,7 +47,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@decky/ui": "^4.7.1", "@decky/ui": "^4.7.0",
"filesize": "^10.1.2", "filesize": "^10.1.2",
"i18next": "^23.11.5", "i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2", "i18next-http-backend": "^2.5.2",
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@decky/ui': '@decky/ui':
specifier: ^4.7.1 specifier: ^4.7.0
version: 4.7.1 version: 4.7.0
filesize: filesize:
specifier: ^10.1.2 specifier: ^10.1.2
version: 10.1.2 version: 10.1.2
@@ -215,8 +215,8 @@ packages:
'@decky/api@1.1.1': '@decky/api@1.1.1':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==} resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
'@decky/ui@4.7.1': '@decky/ui@4.7.0':
resolution: {integrity: sha512-yJwBgW+J2cMDfMkmcDFtzsubhUjekFZAtCnP55QEJ/1UKGR7sLNOvDLFYi1h5PI0K4L1XYcAMKHwbYFFTzcDTA==} resolution: {integrity: sha512-klNWF5tnZVqzuUgFbw+pThiZjK7gKEtwbEZAo4aAuPJSVobpl/euTx9NAxY95QPCFMDgxCo6X6ioEA2nMfHfLA==}
'@esbuild/aix-ppc64@0.20.2': '@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -2289,7 +2289,7 @@ snapshots:
'@decky/api@1.1.1': {} '@decky/api@1.1.1': {}
'@decky/ui@4.7.1': {} '@decky/ui@4.7.0': {}
'@esbuild/aix-ppc64@0.20.2': '@esbuild/aix-ppc64@0.20.2':
optional: true optional: true
+44 -58
View File
@@ -11,62 +11,48 @@ import { visualizer } from 'rollup-plugin-visualizer';
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL']; const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
export default defineConfig([ export default defineConfig({
// Main bundle input: 'src/index.ts',
{ plugins: [
input: 'src/index.ts', del({ targets: '../backend/decky_loader/static/*', force: true }),
plugins: [ commonjs(),
del({ targets: ['../backend/decky_loader/static/*', '!../backend/decky_loader/static/fallback.js'], force: true }), nodeResolve({
commonjs(), browser: true,
nodeResolve({ }),
browser: true, externalGlobals({
}), react: 'SP_REACT',
externalGlobals({ 'react-dom': 'SP_REACTDOM',
react: 'SP_REACT', // hack to shut up react-markdown
'react-dom': 'SP_REACTDOM', process: '{cwd: () => {}}',
// hack to shut up react-markdown path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
process: '{cwd: () => {}}', url: '{fileURLToPath: (f) => f}',
path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}', }),
url: '{fileURLToPath: (f) => f}', typescript(),
}), json(),
typescript(), replace({
json(), preventAssignment: false,
replace({ 'process.env.NODE_ENV': JSON.stringify('production'),
preventAssignment: false, }),
'process.env.NODE_ENV': JSON.stringify('production'), image(),
}), visualizer(),
image(), ],
visualizer(), preserveEntrySignatures: false,
], treeshake: {
preserveEntrySignatures: false, // Assume all external modules have imports with side effects (the default) while allowing decky libraries to treeshake
treeshake: { pureExternalImports: true,
// Assume all external modules have imports with side effects (the default) while allowing decky libraries to treeshake preset: 'smallest'
pureExternalImports: true,
preset: 'smallest'
},
output: {
dir: '../backend/decky_loader/static',
format: 'esm',
chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js';
},
sourcemap: true,
sourcemapPathTransform: (relativeSourcePath) => relativeSourcePath.replace(/^\.\.\//, `decky://decky/loader/`),
},
onwarn: function (message, handleWarning) {
if (hiddenWarnings.some((warning) => message.code === warning)) return;
handleWarning(message);
},
}, },
// Fallback output: {
{ dir: '../backend/decky_loader/static',
input: 'src/fallback.ts', format: 'esm',
plugins: [ chunkFileNames: (chunkInfo) => {
typescript() return 'chunk-[hash].js';
], },
output: { sourcemap: true,
file: '../backend/decky_loader/static/fallback.js', sourcemapPathTransform: (relativeSourcePath) => relativeSourcePath.replace(/^\.\.\//, `decky://decky/loader/`),
format: 'esm', },
} onwarn: function (message, handleWarning) {
} if (hiddenWarnings.some((warning) => message.code === warning)) return;
]); handleWarning(message);
},
});
@@ -94,7 +94,7 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
style={{ marginRight: '5px', padding: '5px' }} style={{ marginRight: '5px', padding: '5px' }}
onClick={() => { onClick={() => {
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart();
}} }}
> >
Restart Steam Restart Steam
@@ -121,7 +121,7 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
doShutdown(); doShutdown();
await sleep(5000); await sleep(5000);
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart();
}} }}
> >
Disable Decky until next boot Disable Decky until next boot
@@ -166,7 +166,7 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
await sleep(2000); await sleep(2000);
addLogLine('Restarting Steam...'); addLogLine('Restarting Steam...');
await sleep(500); await sleep(500);
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart();
}} }}
> >
Uninstall {errorSource} and restart Decky Uninstall {errorSource} and restart Decky
-128
View File
@@ -1,128 +0,0 @@
// 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);
}
})();
+1 -30
View File
@@ -172,32 +172,11 @@ class PluginLoader extends Logger {
.then(() => this.log('Initialized')); .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< private getPluginsFromBackend = DeckyBackend.callable<
[], [],
{ name: string; version: string; load_type: PluginLoadType }[] { name: string; version: string; load_type: PluginLoadType }[]
>('loader/get_plugins'); >('loader/get_plugins');
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
private async loadPlugins() { private async loadPlugins() {
let registration: any; let registration: any;
const uiMode = await new Promise( const uiMode = await new Promise(
@@ -213,7 +192,6 @@ class PluginLoader extends Logger {
await sleep(100); await sleep(100);
} }
} }
this.runCrashChecker();
const plugins = await this.getPluginsFromBackend(); const plugins = await this.getPluginsFromBackend();
const pluginLoadPromises = []; const pluginLoadPromises = [];
const loadStart = performance.now(); const loadStart = performance.now();
@@ -417,7 +395,6 @@ class PluginLoader extends Logger {
version?: string, version?: string,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
) { ) {
let spExists = this.checkForSP();
try { try {
switch (loadType) { switch (loadType) {
case PluginLoadType.ESMODULE_V1: case PluginLoadType.ESMODULE_V1:
@@ -465,7 +442,7 @@ class PluginLoader extends Logger {
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<pre style={{ overflowX: 'scroll' }}> <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> </pre>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
@@ -497,12 +474,6 @@ class PluginLoader extends Logger {
icon: <FaExclamationCircle />, 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 = {}) { async callServerMethod(methodName: string, args = {}) {
+4 -5
View File
@@ -1,6 +1,5 @@
// import restartFix from './restart'; // import reloadFix from './reload';
import cefSocketFix from './socket'; import restartFix from './restart';
let fixes: Function[] = []; let fixes: Function[] = [];
export function deinitSteamFixes() { export function deinitSteamFixes() {
@@ -8,6 +7,6 @@ export function deinitSteamFixes() {
} }
export async function initSteamFixes() { export async function initSteamFixes() {
fixes.push(cefSocketFix()); // fixes.push(await reloadFix());
// fixes.push(await restartFix()); fixes.push(await restartFix());
} }
-16
View File
@@ -1,16 +0,0 @@
import Logger from '../logger';
const logger = new Logger('CEFSocketFix');
const closeCEFSocket = DeckyBackend.callable<[], void>('utilities/close_cef_socket');
export default function cefSocketFix() {
const reg = window.SteamClient?.User?.RegisterForShutdownStart(async () => {
logger.log('Closing CEF socket before shutdown');
await closeCEFSocket();
});
if (reg) logger.debug('CEF shutdown handler ready');
return () => reg?.unregister();
}
+3 -16
View File
@@ -1,13 +1,5 @@
import type { ToastData, ToastNotification } from '@decky/api'; import type { ToastData, ToastNotification } from '@decky/api';
import { import { Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui';
ErrorBoundary,
Patch,
callOriginal,
findModuleExport,
injectFCTrampoline,
replacePatch,
sleep,
} from '@decky/ui';
import Toast from './components/Toast'; import Toast from './components/Toast';
import Logger from './logger'; import Logger from './logger';
@@ -29,8 +21,6 @@ declare global {
class Toaster extends Logger { class Toaster extends Logger {
private toastPatch?: Patch; private toastPatch?: Patch;
private markReady!: () => void;
private ready = new Promise<void>((r) => (this.markReady = r));
constructor() { constructor() {
super('Toaster'); super('Toaster');
@@ -44,16 +34,13 @@ class Toaster extends Logger {
this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => { this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => {
if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) { if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) {
return args[0].group.notifications.map((notification: any) => ( return args[0].group.notifications.map((notification: any) => (
<ErrorBoundary> <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
<Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
</ErrorBoundary>
)); ));
} }
return callOriginal; return callOriginal;
}); });
this.log('Initialized'); this.log('Initialized');
sleep(4000).then(this.markReady);
} }
toast(toast: ToastData): ToastNotification { toast(toast: ToastData): ToastNotification {
@@ -120,7 +107,7 @@ class Toaster extends Logger {
} }
}, toast.expiration); }, toast.expiration);
} }
this.ready.then(() => window.NotificationStore.ProcessNotification(info, toastData, ToastType.New)); window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
return toastResult; return toastResult;
} }