mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 08:12:03 +00:00
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:
Vendored
+32
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user