Refactoring in preparation for WebSockets (#254)

* Fix injector race conditions

* add some more tasks

* hide useless rollup warnings

* goodbye to clientsession errors

* completely fix desktop mode switch race condition

* fix typos and TS warning in plugin error handler

* fix chown error

* start debugger if needed and not already started

* fix get_steam_resource for the like 2 legacy plugins still using it lol

* add ClientOSError to get_tabs error handling
This commit is contained in:
AAGaming
2022-11-15 16:44:24 -05:00
committed by GitHub
parent aec7063139
commit e803f01efe
9 changed files with 295 additions and 228 deletions
+32 -5
View File
@@ -14,7 +14,9 @@
"label": "localrun", "label": "localrun",
"type": "shell", "type": "shell",
"group": "none", "group": "none",
"dependsOn" : ["buildall"], "dependsOn": [
"buildall"
],
"detail": "Check for local runs, create a plugins folder", "detail": "Check for local runs, create a plugins folder",
"command": "mkdir -p plugins", "command": "mkdir -p plugins",
"problemMatcher": [] "problemMatcher": []
@@ -48,6 +50,16 @@
"command": "cd frontend && pnpm i", "command": "cd frontend && pnpm i",
"problemMatcher": [] "problemMatcher": []
}, },
{
"script": "watch",
"type": "npm",
"path": "frontend",
"group": "build",
"problemMatcher": [],
"label": "watchfrontend",
"detail": "rollup -c -w",
"isBackground": true
},
{ {
"label": "buildfrontend", "label": "buildfrontend",
"type": "npm", "type": "npm",
@@ -55,8 +67,7 @@
"detail": "rollup -c", "detail": "rollup -c",
"script": "build", "script": "build",
"path": "frontend", "path": "frontend",
"problemMatcher": [], "problemMatcher": []
}, },
{ {
"label": "buildall", "label": "buildall",
@@ -95,7 +106,9 @@
"detail": "Run indev PluginLoader on Deck", "detail": "Run indev PluginLoader on Deck",
"type": "shell", "type": "shell",
"group": "none", "group": "none",
"dependsOn" : ["checkforsettings"], "dependsOn": [
"checkforsettings"
],
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'", "command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
"problemMatcher": [] "problemMatcher": []
}, },
@@ -108,6 +121,20 @@
"problemMatcher": [] "problemMatcher": []
}, },
// ALL-IN-ONES // ALL-IN-ONES
{
"label": "deployandrun",
"detail": "Deploy and run, skipping JS build. Useful when combined with npm:watch",
"dependsOrder": "sequence",
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"deploy",
"runpydeck"
],
"problemMatcher": []
},
{ {
"label": "updateremote", "label": "updateremote",
"detail": "Build and deploy", "detail": "Build and deploy",
@@ -115,7 +142,7 @@
"group": "none", "group": "none",
"dependsOn": [ "dependsOn": [
"buildall", "buildall",
"deploy", "deploy"
], ],
"problemMatcher": [] "problemMatcher": []
}, },
+23 -4
View File
@@ -6,7 +6,7 @@ from traceback import format_exc
from typing import List from typing import List
from aiohttp import ClientSession, WSMsgType from aiohttp import ClientSession, WSMsgType
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
from asyncio.exceptions import TimeoutError from asyncio.exceptions import TimeoutError
import uuid import uuid
@@ -32,12 +32,15 @@ class Tab:
self.websocket = await self.client.ws_connect(self.ws_url) self.websocket = await self.client.ws_connect(self.ws_url)
async def close_websocket(self): async def close_websocket(self):
await self.websocket.close()
await self.client.close() await self.client.close()
async def listen_for_message(self): async def listen_for_message(self):
async for message in self.websocket: async for message in self.websocket:
data = message.json() data = message.json()
yield data yield data
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
await self.close_websocket()
async def _send_devtools_cmd(self, dc, receive=True): async def _send_devtools_cmd(self, dc, receive=True):
if self.websocket: if self.websocket:
@@ -52,6 +55,7 @@ class Tab:
raise RuntimeError("Websocket not opened") raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True): async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
try:
if manage_socket: if manage_socket:
await self.open_websocket() await self.open_websocket()
@@ -64,6 +68,7 @@ class Tab:
} }
}, get_result) }, get_result)
finally:
if manage_socket: if manage_socket:
await self.close_websocket() await self.close_websocket()
return res return res
@@ -77,6 +82,7 @@ class Tab:
return res["result"]["result"]["value"] return res["result"]["result"]["value"]
async def close(self, manage_socket=True): async def close(self, manage_socket=True):
try:
if manage_socket: if manage_socket:
await self.open_websocket() await self.open_websocket()
@@ -84,6 +90,7 @@ class Tab:
"method": "Page.close", "method": "Page.close",
}, False) }, False)
finally:
if manage_socket: if manage_socket:
await self.close_websocket() await self.close_websocket()
return res return res
@@ -105,6 +112,7 @@ class Tab:
}, False) }, False)
async def refresh(self): async def refresh(self):
try:
if manage_socket: if manage_socket:
await self.open_websocket() await self.open_websocket()
@@ -112,6 +120,7 @@ class Tab:
"method": "Page.reload", "method": "Page.reload",
}, False) }, False)
finally:
if manage_socket: if manage_socket:
await self.close_websocket() await self.close_websocket()
@@ -120,6 +129,7 @@ class Tab:
""" """
Reloads the current tab, with JS to run on load via debugger Reloads the current tab, with JS to run on load via debugger
""" """
try:
if manage_socket: if manage_socket:
await self.open_websocket() await self.open_websocket()
@@ -175,6 +185,7 @@ class Tab:
"method": "Debugger.disable" "method": "Debugger.disable"
}, True) }, True)
finally:
if manage_socket: if manage_socket:
await self.close_websocket() await self.close_websocket()
return return
@@ -212,6 +223,7 @@ class Tab:
(see remove_script_to_evaluate_on_new_document below) (see remove_script_to_evaluate_on_new_document below)
None is returned if `get_result` is False None is returned if `get_result` is False
""" """
try:
wrappedjs = """ wrappedjs = """
function scriptFunc() { function scriptFunc() {
@@ -236,6 +248,7 @@ class Tab:
} }
}, get_result) }, get_result)
finally:
if manage_socket: if manage_socket:
await self.close_websocket() await self.close_websocket()
return res return res
@@ -250,6 +263,7 @@ class Tab:
The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`) The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
""" """
try:
if manage_socket: if manage_socket:
await self.open_websocket() await self.open_websocket()
@@ -260,6 +274,7 @@ class Tab:
} }
}, False) }, False)
finally:
if manage_socket: if manage_socket:
await self.close_websocket() await self.close_websocket()
@@ -337,17 +352,21 @@ class Tab:
async def get_tabs() -> List[Tab]: async def get_tabs() -> List[Tab]:
async with ClientSession() as web:
res = {} res = {}
na = False
while True: while True:
try: try:
async with ClientSession() as web:
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3) res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
except ClientConnectorError: except ClientConnectorError:
logger.debug("ClientConnectorError excepted.") if not na:
logger.debug("Steam isn't available yet. Wait for a moment...") logger.debug("Steam isn't available yet. Wait for a moment...")
logger.error(format_exc()) na = True
await sleep(5) await sleep(5)
except ClientOSError:
logger.warn(f"The request to {BASE_ADDRESS}/json was reset")
await sleep(1)
except TimeoutError: except TimeoutError:
logger.warn(f"The request to {BASE_ADDRESS}/json timed out") logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
await sleep(1) await sleep(1)
+1 -1
View File
@@ -208,7 +208,7 @@ class Loader:
return web.Response(text=ret) return web.Response(text=ret)
async def get_steam_resource(self, request): async def get_steam_resource(self, request):
tab = await get_tab("QuickAccess") tab = await get_tab("SP")
try: try:
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html") return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
except Exception as e: except Exception as e:
+38 -26
View File
@@ -4,7 +4,7 @@ from subprocess import call
if hasattr(sys, '_MEIPASS'): if hasattr(sys, '_MEIPASS'):
call(['chmod', '-R', '755', sys._MEIPASS]) call(['chmod', '-R', '755', sys._MEIPASS])
# Full imports # Full imports
from asyncio import get_event_loop, sleep from asyncio import new_event_loop, set_event_loop, sleep
from json import dumps, loads from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, chmod, path from os import getenv, chmod, path
@@ -12,7 +12,7 @@ from traceback import format_exc
import aiohttp_cors import aiohttp_cors
# Partial imports # Partial imports
from aiohttp import ClientSession, client_exceptions, WSMsgType from aiohttp import client_exceptions, WSMsgType
from aiohttp.web import Application, Response, get, run_app, static from aiohttp.web import Application, Response, get, run_app, static
from aiohttp_jinja2 import setup as jinja_setup from aiohttp_jinja2 import setup as jinja_setup
@@ -21,7 +21,7 @@ from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
get_home_path, get_homebrew_path, get_user, get_home_path, get_homebrew_path, get_user,
get_user_group, set_user, set_user_group, get_user_group, set_user, set_user_group,
stop_systemd_unit) stop_systemd_unit, start_systemd_unit)
from injector import get_gamepadui_tab, Tab, get_tabs from injector import get_gamepadui_tab, Tab, get_tabs
from loader import Loader from loader import Loader
from settings import SettingsManager from settings import SettingsManager
@@ -56,15 +56,15 @@ basicConfig(
logger = getLogger("Main") logger = getLogger("Main")
async def chown_plugin_dir(_): async def chown_plugin_dir():
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]]) code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]]) code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
if code_chown != 0 or code_chmod != 0: if code_chown != 0 or code_chmod != 0:
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})") logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
class PluginManager: class PluginManager:
def __init__(self) -> None: def __init__(self, loop) -> None:
self.loop = get_event_loop() self.loop = loop
self.web_app = Application() self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware) self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={ self.cors = aiohttp_cors.setup(self.web_app, defaults={
@@ -81,12 +81,19 @@ class PluginManager:
self.updater = Updater(self) self.updater = Updater(self)
jinja_setup(self.web_app) jinja_setup(self.web_app)
async def startup(_):
if self.settings.getSetting("cef_forward", False):
self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
else:
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
if CONFIG["chown_plugin_path"] == True: if CONFIG["chown_plugin_path"] == True:
self.web_app.on_startup.append(chown_plugin_dir) chown_plugin_dir()
self.loop.create_task(self.loader_reinjector()) self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins()) self.loop.create_task(self.load_plugins())
if not self.settings.getSetting("cef_forward", False):
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT)) self.web_app.on_startup.append(startup)
self.loop.set_exception_handler(self.exception_handler) self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)]) self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
@@ -103,31 +110,29 @@ class PluginManager:
async def get_auth_token(self, request): async def get_auth_token(self, request):
return Response(text=get_csrf_token()) return Response(text=get_csrf_token())
async def wait_for_server(self):
async with ClientSession() as web:
while True:
try:
await web.get(f"http://{CONFIG['server_host']}:{CONFIG['server_port']}")
return
except Exception as e:
await sleep(0.1)
async def load_plugins(self): async def load_plugins(self):
await self.wait_for_server() # await self.wait_for_server()
logger.debug("Loading plugins")
self.plugin_loader.import_plugins() self.plugin_loader.import_plugins()
# await inject_to_tab("SP", "window.syncDeckyPlugins();") # await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def loader_reinjector(self): async def loader_reinjector(self):
while True: while True:
tab = None tab = None
nf = False
dc = False
while not tab: while not tab:
try: try:
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError: except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError:
logger.debug("Couldn't connect to debugger, waiting 5 seconds.") if not dc:
logger.debug("Couldn't connect to debugger, waiting...")
dc = True
pass pass
except ValueError: except ValueError:
logger.debug("Couldn't find GamepadUI tab, waiting 5 seconds") if not nf:
logger.debug("Couldn't find GamepadUI tab, waiting...")
nf = True
pass pass
if not tab: if not tab:
await sleep(5) await sleep(5)
@@ -136,15 +141,20 @@ class PluginManager:
await self.inject_javascript(tab, True) await self.inject_javascript(tab, True)
try: try:
async for msg in tab.listen_for_message(): async for msg in tab.listen_for_message():
# this gets spammed a lot
if msg.get("method", None) != "Page.navigatedWithinDocument":
logger.debug("Page event: " + str(msg.get("method", None))) logger.debug("Page event: " + str(msg.get("method", None)))
if msg.get("method", None) == "Page.domContentEventFired": if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False): if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab) await self.inject_javascript(tab)
if msg.get("method", None) == "Inspector.detached" or msg.get("type", None) in (WSMsgType.CLOSED, WSMsgType.ERROR): if msg.get("method", None) == "Inspector.detached":
logger.info("CEF has disconnected...") logger.info("CEF has requested that we detach.")
logger.debug("Exit message: " + str(msg))
await tab.close_websocket() await tab.close_websocket()
break break
# 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
logger.info("CEF has disconnected...")
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
except Exception as e: except Exception as e:
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()
@@ -166,7 +176,7 @@ class PluginManager:
# logger.debug("Closing tab: " + getattr(t, "title", "Untitled")) # logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
# await t.close() # await t.close()
# await sleep(0.5) # await sleep(0.5)
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.User.StartRestart(), 100)}else{window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}}catch(e){console.error(e)}", False, False, False) await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.User.StartRestart(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
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
@@ -175,4 +185,6 @@ class PluginManager:
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None) return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
if __name__ == "__main__": if __name__ == "__main__":
PluginManager().run() loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
+1 -1
View File
@@ -198,7 +198,7 @@ class Updater:
logger.info("Updated loader installation.") logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False) await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await self.do_restart() await self.do_restart()
await tab.client.close() await tab.close_websocket()
async def do_restart(self): async def do_restart(self):
call(["systemctl", "daemon-reload"]) call(["systemctl", "daemon-reload"])
+2 -2
View File
@@ -80,7 +80,7 @@ class Utilities:
async def http_request(self, method="", url="", **kwargs): async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web: async with ClientSession() as web:
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res: res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
return { return {
"status": res.status, "status": res.status,
"headers": dict(res.headers), "headers": dict(res.headers),
@@ -241,7 +241,7 @@ class Utilities:
if ip != None: if ip != None:
self.logger.info("Connecting to React DevTools at " + ip) self.logger.info("Connecting to React DevTools at " + ip)
async with ClientSession() as web: async with ClientSession() as web:
async with web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) as res: res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
if res.status != 200: if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip) self.logger.error("Failed to connect to React DevTools at " + ip)
return False return False
+10 -1
View File
@@ -5,7 +5,12 @@ import externalGlobals from "rollup-plugin-external-globals";
import del from 'rollup-plugin-delete' import del from 'rollup-plugin-delete'
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup'; import { defineConfig, handleWarning } from 'rollup';
const hiddenWarnings = [
"THIS_IS_UNDEFINED",
"EVAL"
];
export default defineConfig({ export default defineConfig({
input: 'src/index.tsx', input: 'src/index.tsx',
@@ -35,5 +40,9 @@ export default defineConfig({
chunkFileNames: (chunkInfo) => { chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js' return 'chunk-[hash].js'
} }
},
onwarn: function ( message ) {
if (hiddenWarnings.some(warning => message.code === warning)) return;
handleWarning(message);
} }
}); });
+3 -3
View File
@@ -249,11 +249,11 @@ class PluginLoader extends Logger {
<> <>
Error:{' '} Error:{' '}
<pre> <pre>
<code>{e instanceof Error ? e.stack : e?.toString()}</code> <code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
</pre> </pre>
<> <>
Please go to <FaCog style={{ display: 'inline' }} /> in Decky Loader.e settings menu if you need to Please go to <FaCog style={{ display: 'inline' }} /> in the Decky menu if you need to uninstall this
uninstall this plugin. plugin.
</> </>
</> </>
); );