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 50764600c8
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": []
}, },
+11 -11
View File
@@ -94,18 +94,18 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
if os.access(os.path.dirname(path), os.W_OK): if os.access(os.path.dirname(path), os.W_OK):
async with ClientSession() as client: async with ClientSession() as client:
res = await client.get(url, ssl=get_ssl_context()) res = await client.get(url, ssl=get_ssl_context())
if res.status == 200: if res.status == 200:
data = BytesIO(await res.read()) data = BytesIO(await res.read())
remoteHash = sha256(data.getbuffer()).hexdigest() remoteHash = sha256(data.getbuffer()).hexdigest()
if binHash == remoteHash: if binHash == remoteHash:
data.seek(0) data.seek(0)
with open(path, 'wb') as f: with open(path, 'wb') as f:
f.write(data.getbuffer()) f.write(data.getbuffer())
rv = True rv = True
else:
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
else: else:
rv = False raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
else:
rv = False
except: except:
rv = False rv = False
+139 -120
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,20 +55,22 @@ 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):
if manage_socket: try:
await self.open_websocket() if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({ res = await self._send_devtools_cmd({
"method": "Runtime.evaluate", "method": "Runtime.evaluate",
"params": { "params": {
"expression": js, "expression": js,
"userGesture": True, "userGesture": True,
"awaitPromise": run_async "awaitPromise": run_async
} }
}, get_result) }, get_result)
if manage_socket: finally:
await self.close_websocket() if manage_socket:
await self.close_websocket()
return res return res
async def has_global_var(self, var_name, manage_socket=True): async def has_global_var(self, var_name, manage_socket=True):
@@ -77,15 +82,17 @@ 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):
if manage_socket: try:
await self.open_websocket() if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({ res = await self._send_devtools_cmd({
"method": "Page.close", "method": "Page.close",
}, False) }, False)
if manage_socket: finally:
await self.close_websocket() if manage_socket:
await self.close_websocket()
return res return res
async def enable(self): async def enable(self):
@@ -105,78 +112,82 @@ class Tab:
}, False) }, False)
async def refresh(self): async def refresh(self):
if manage_socket: try:
await self.open_websocket() if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({ await self._send_devtools_cmd({
"method": "Page.reload", "method": "Page.reload",
}, False) }, False)
if manage_socket: finally:
await self.close_websocket() if manage_socket:
await self.close_websocket()
return return
async def reload_and_evaluate(self, js, manage_socket=True): async def reload_and_evaluate(self, js, manage_socket=True):
""" """
Reloads the current tab, with JS to run on load via debugger Reloads the current tab, with JS to run on load via debugger
""" """
if manage_socket: try:
await self.open_websocket() if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({ await self._send_devtools_cmd({
"method": "Debugger.enable" "method": "Debugger.enable"
}, True) }, True)
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": "location.reload();",
"userGesture": True,
"awaitPromise": False
}
}, False)
breakpoint_res = await self._send_devtools_cmd({
"method": "Debugger.setInstrumentationBreakpoint",
"params": {
"instrumentation": "beforeScriptExecution"
}
}, True)
logger.info(breakpoint_res)
# Page finishes loading when breakpoint hits
for x in range(20):
# this works around 1/5 of the time, so just send it 8 times.
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
await self._send_devtools_cmd({ await self._send_devtools_cmd({
"method": "Runtime.evaluate", "method": "Runtime.evaluate",
"params": { "params": {
"expression": js, "expression": "location.reload();",
"userGesture": True, "userGesture": True,
"awaitPromise": False "awaitPromise": False
} }
}, False) }, False)
await self._send_devtools_cmd({ breakpoint_res = await self._send_devtools_cmd({
"method": "Debugger.removeBreakpoint", "method": "Debugger.setInstrumentationBreakpoint",
"params": { "params": {
"breakpointId": breakpoint_res["result"]["breakpointId"] "instrumentation": "beforeScriptExecution"
} }
}, False) }, True)
logger.info(breakpoint_res)
# Page finishes loading when breakpoint hits
for x in range(20):
# this works around 1/5 of the time, so just send it 8 times.
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": False
}
}, False)
for x in range(4):
await self._send_devtools_cmd({ await self._send_devtools_cmd({
"method": "Debugger.resume" "method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": breakpoint_res["result"]["breakpointId"]
}
}, False) }, False)
await self._send_devtools_cmd({ for x in range(4):
"method": "Debugger.disable" await self._send_devtools_cmd({
}, True) "method": "Debugger.resume"
}, False)
if manage_socket: await self._send_devtools_cmd({
await self.close_websocket() "method": "Debugger.disable"
}, True)
finally:
if manage_socket:
await self.close_websocket()
return return
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True): async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
@@ -212,32 +223,34 @@ 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() {
{js} {js}
}
if (document.readyState === 'loading') {
addEventListener('DOMContentLoaded', () => {
scriptFunc();
});
} else {
scriptFunc();
}
""".format(js=js) if add_dom_wrapper else js
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.addScriptToEvaluateOnNewDocument",
"params": {
"source": wrappedjs
} }
}, get_result) if (document.readyState === 'loading') {
addEventListener('DOMContentLoaded', () => {
scriptFunc();
});
} else {
scriptFunc();
}
""".format(js=js) if add_dom_wrapper else js
if manage_socket: if manage_socket:
await self.close_websocket() await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.addScriptToEvaluateOnNewDocument",
"params": {
"source": wrappedjs
}
}, get_result)
finally:
if manage_socket:
await self.close_websocket()
return res return res
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True): async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
@@ -250,18 +263,20 @@ 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`)
""" """
if manage_socket: try:
await self.open_websocket() if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({ res = await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument", "method": "Page.removeScriptToEvaluateOnNewDocument",
"params": { "params": {
"identifier": script_id "identifier": script_id
} }
}, False) }, False)
if manage_socket: finally:
await self.close_websocket() if manage_socket:
await self.close_websocket()
async def has_element(self, element_name, manage_socket=True): async def has_element(self, element_name, manage_socket=True):
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket) res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
@@ -337,28 +352,32 @@ class Tab:
async def get_tabs() -> List[Tab]: async def get_tabs() -> List[Tab]:
async with ClientSession() as web: res = {}
res = {}
while True: na = False
try: while True:
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 TimeoutError: except ClientOSError:
logger.warn(f"The request to {BASE_ADDRESS}/json timed out") logger.warn(f"The request to {BASE_ADDRESS}/json was reset")
await sleep(1) await sleep(1)
else: except TimeoutError:
break logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
await sleep(1)
if res.status == 200:
r = await res.json()
return [Tab(i) for i in r]
else: else:
raise Exception(f"/json did not return 200. {await res.text()}") break
if res.status == 200:
r = await res.json()
return [Tab(i) for i in r]
else:
raise Exception(f"/json did not return 200. {await res.text()}")
async def get_tab(tab_name) -> Tab: async def get_tab(tab_name) -> Tab:
+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:
+47 -35
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)
if CONFIG["chown_plugin_path"] == True:
self.web_app.on_startup.append(chown_plugin_dir) async def startup(_):
self.loop.create_task(self.loader_reinjector()) if self.settings.getSetting("cef_forward", False):
self.loop.create_task(self.load_plugins()) self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
if not self.settings.getSetting("cef_forward", False): else:
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT)) self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
if CONFIG["chown_plugin_path"] == True:
chown_plugin_dir()
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
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():
logger.debug("Page event: " + str(msg.get("method", None))) # this gets spammed a lot
if msg.get("method", None) == "Page.domContentEventFired": if msg.get("method", None) != "Page.navigatedWithinDocument":
if not await tab.has_global_var("deckyHasLoaded", False): logger.debug("Page event: " + str(msg.get("method", None)))
await self.inject_javascript(tab) if msg.get("method", None) == "Page.domContentEventFired":
if msg.get("method", None) == "Inspector.detached" or msg.get("type", None) in (WSMsgType.CLOSED, WSMsgType.ERROR): if not await tab.has_global_var("deckyHasLoaded", False):
logger.info("CEF has disconnected...") await self.inject_javascript(tab)
logger.debug("Exit message: " + str(msg)) if msg.get("method", None) == "Inspector.detached":
await tab.close_websocket() logger.info("CEF has requested that we detach.")
break await tab.close_websocket()
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()
+33 -33
View File
@@ -112,20 +112,20 @@ class Updater:
async with ClientSession() as web: async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res: async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json() remoteVersions = await res.json()
self.allRemoteVers = remoteVersions self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch) logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0: if selectedBranch == 0:
logger.debug("release type: release") logger.debug("release type: release")
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None) self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None)
elif selectedBranch == 1: elif selectedBranch == 1:
logger.debug("release type: pre-release") logger.debug("release type: pre-release")
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None) self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
else: else:
logger.error("release type: NOT FOUND") logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found") raise ValueError("no valid branch found")
logger.info("Updated remote version information") logger.info("Updated remote version information")
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False) await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
return await self.get_version() return await self.get_version()
async def version_reloader(self): async def version_reloader(self):
@@ -152,18 +152,18 @@ class Updater:
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res: async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
logger.debug("Downloading service file") logger.debug("Downloading service file")
data = await res.content.read() data = await res.content.read()
logger.debug(str(data)) logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service") service_file_path = path.join(getcwd(), "plugin_loader.service")
try: try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out: with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data) out.write(data)
except Exception as e: except Exception as e:
logger.error(f"Error at %s", exc_info=e) logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file: with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file:
service_data = service_file.read() service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew") service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file: with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file:
service_file.write(service_data) service_file.write(service_data)
logger.debug("Saved service file") logger.debug("Saved service file")
logger.debug("Copying service file over current file.") logger.debug("Copying service file over current file.")
@@ -191,14 +191,14 @@ class Updater:
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False)) self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
progress = new_progress progress = new_progress
with open(path.join(getcwd(), ".loader.version"), "w") as out: with open(path.join(getcwd(), ".loader.version"), "w") as out:
out.write(version) out.write(version)
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")]) call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
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"])
+17 -17
View File
@@ -80,12 +80,12 @@ 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),
"body": await res.text() "body": await res.text()
} }
async def ping(self, **kwargs): async def ping(self, **kwargs):
return "pong" return "pong"
@@ -241,17 +241,17 @@ 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
self.start_rdt_proxy(ip, 8097) self.start_rdt_proxy(ip, 8097)
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}" script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
self.logger.info("Connected to React DevTools, loading script") self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work. # RDT needs to load before React itself to work.
result = await tab.reload_and_evaluate(script) result = await tab.reload_and_evaluate(script)
self.logger.info(result) self.logger.info(result)
except Exception: except Exception:
self.logger.error("Failed to connect to React DevTools") self.logger.error("Failed to connect to React DevTools")
+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.
</> </>
</> </>
); );