Compare commits

..

26 Commits

Author SHA1 Message Date
TrainDoctor 3a927084b0 FIlled out functionality of handler function, now for JS to call route 2022-12-10 14:25:21 -08:00
TrainDoctor 5e07c8f028 Fix a typo 2022-12-10 14:25:20 -08:00
TrainDoctor 91d1bc8a78 Improvements to install of remote binaries, and start backend reloading 2022-12-10 14:25:20 -08:00
TrainDoctor ea35af2050 Update build.yml 2022-12-08 15:18:44 -08:00
NGnius (Graham) 6232e3da58 Add custom CDN support for custom stores (#269)
* Add custom CDN support for custom stores

* Update Python for CI
2022-12-07 16:27:32 -08:00
TrainDoctor 35e46f9ccb Update build.yml 2022-12-07 14:31:09 -08:00
TrainDoctor 2b9a80c151 Update install_prerelease.sh 2022-12-04 19:05:29 -08:00
TrainDoctor a90ed38c89 Update install_release.sh 2022-12-04 19:05:16 -08:00
TrainDoctor 3653cf5640 Update plugin_loader-release.service 2022-12-04 19:05:01 -08:00
TrainDoctor 0db45ca71e Update plugin_loader-prerelease.service 2022-12-04 19:04:46 -08:00
AAGaming 16681fabb5 fix http requests 2022-11-19 22:33:51 -05:00
AAGaming c210523a22 fix handleWarning in rollup config 2022-11-19 20:07:08 -05:00
Marco Rodolfi 5d8601347a Fix for wrong path for settings json files (#258)
Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-11-19 19:34:38 -05:00
AAGaming 1e02fcf394 fix broken trycatch causing occasional injection failures 2022-11-19 19:22:30 -05:00
TrainDoctor f923306a7f Update issue templates 2022-11-19 14:19:33 -08:00
TrainDoctor 478fe32527 Revert "Fix for setting json files ending up in ~/homebrew"
This reverts commit aec7063139.
2022-11-15 15:01:26 -08:00
AAGaming 50764600c8 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
2022-11-15 13:44:24 -08:00
TrainDoctor aec7063139 Fix for setting json files ending up in ~/homebrew 2022-11-13 17:48:46 -08:00
TrainDoctor c9ee98e0c0 Fix desktop mode loop (#253)
* fix desktop mode making injector get stuck

* Fix imports and variable references

* Get data for all messages

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-11-13 17:36:00 -08:00
TrainDoctor 093b064a4e Inline icon propely 2022-11-13 16:44:15 -08:00
TrainDoctor 2955681975 Show plugin as error and give guidance for uninstall 2022-11-13 16:31:05 -08:00
TrainDoctor de42639726 Add errored plugins to plugin array for uninstalls 2022-11-13 16:01:06 -08:00
TrainDoctor 17742e947a Fix import revert 2022-11-08 17:05:10 -08:00
TrainDoctor 898271b33d Revert "Disable store selection until PRs actually deploy to testing"
This reverts commit d389b403b5.
2022-11-06 15:37:02 -08:00
AAGaming b44896524f Fix on steam client beta, restart steam instead of reloading tab since that is broken 2022-11-04 21:49:05 -04:00
TrainDoctor db7bb236d8 facepalm 2022-10-30 18:45:18 -07:00
22 changed files with 477 additions and 302 deletions
+36
View File
@@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Description**
[A clear and concise description of what the bug is.]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
[A clear and concise description of what you expected to happen.]
**Screenshots**
[If applicable, add screenshots to help explain your problem.]
**Version information**
- SteamOS Version: ``[Run ``uname -a`` and place the output here. Leave the single quotations outside.]``
- Selected Update Channel: [Stable, Beta or Preview.]
**Logs**
[Please reboot your deck (if possible) when attempting to recreate the issue, then run
``cd ~ && journalctl -b0 -u plugin_loader.service > backendlog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here in place of this textblock.]
**Additional context**
Have you modified the read-only filesystem at any point?
[Yes or No.]
+3 -3
View File
@@ -31,7 +31,7 @@ permissions:
jobs:
build:
name: Build PluginLoader
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Print input
@@ -47,10 +47,10 @@ jobs:
with:
node-version: 18
- name: Set up Python 3.10.2 🐍
- name: Set up Python 3.10.9 🐍
uses: actions/setup-python@v4
with:
python-version: "3.10.2"
python-version: "3.10.9"
- name: Install Python dependencies ⬇️
run: |
+33 -6
View File
@@ -14,7 +14,9 @@
"label": "localrun",
"type": "shell",
"group": "none",
"dependsOn" : ["buildall"],
"dependsOn": [
"buildall"
],
"detail": "Check for local runs, create a plugins folder",
"command": "mkdir -p plugins",
"problemMatcher": []
@@ -48,6 +50,16 @@
"command": "cd frontend && pnpm i",
"problemMatcher": []
},
{
"script": "watch",
"type": "npm",
"path": "frontend",
"group": "build",
"problemMatcher": [],
"label": "watchfrontend",
"detail": "rollup -c -w",
"isBackground": true
},
{
"label": "buildfrontend",
"type": "npm",
@@ -55,8 +67,7 @@
"detail": "rollup -c",
"script": "build",
"path": "frontend",
"problemMatcher": [],
"problemMatcher": []
},
{
"label": "buildall",
@@ -95,7 +106,9 @@
"detail": "Run indev PluginLoader on Deck",
"type": "shell",
"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'",
"problemMatcher": []
},
@@ -108,6 +121,20 @@
"problemMatcher": []
},
// 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",
"detail": "Build and deploy",
@@ -115,7 +142,7 @@
"group": "none",
"dependsOn": [
"buildall",
"deploy",
"deploy"
],
"problemMatcher": []
},
@@ -152,4 +179,4 @@
"problemMatcher": []
}
]
}
}
+1 -1
View File
@@ -98,7 +98,7 @@ Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loa
1. Use the VS Code tasks or `deck.sh` script to deploy your changes to your Steam Deck to test them.
1. You will be testing your changes with the Python script version. You will need to build, deploy, and reload each time.
⚠️ If you are recieving build errors due to an out of date library, you should run this command inside of your repository.
⚠️ If you are receiving build errors due to an out of date library, you should run this command inside of your repository.
```bash
pnpm update decky-frontend-lib --latest
+7
View File
@@ -60,6 +60,9 @@ class PluginBrowser:
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
# create bin directory if needed.
rc=call(["chmod", "-R", "777", pluginBasePath])
if rc != 0:
logger.error(f"chown of plugin w/ remote binary exited with a non-zero exit code, chmod: {rc})")
if access(pluginBasePath, W_OK):
if not path.exists(pluginBinPath):
@@ -67,6 +70,8 @@ class PluginBrowser:
if not access(pluginBinPath, W_OK):
rc=call(["chmod", "-R", "777", pluginBinPath])
if rc != 0:
logger.error(f"chown of plugin w/ remote binary exited with a non-zero exit code, chmod: {rc})")
rv = True
for remoteBinary in packageJson["remote_binary"]:
@@ -80,6 +85,8 @@ class PluginBrowser:
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
rc=call(["chmod", "-R", "555", pluginBasePath])
if code_chown or rc != 0:
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {rc})")
else:
rv = True
logger.debug(f"No Remote Binaries to Download")
+21 -11
View File
@@ -1,3 +1,5 @@
import grp
import pwd
import re
import ssl
import subprocess
@@ -56,6 +58,14 @@ def get_user() -> str:
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
return user
#Get the user owner of the given file path.
def get_user_owner(file_path) -> str:
return pwd.getpwuid(os.stat(file_path).st_uid)[0]
#Get the user group of the given file path.
def get_user_group(file_path) -> str:
return grp.getgrgid(os.stat(file_path).st_gid)[0]
# Set the global user group. get_user must be called first
def set_user_group() -> str:
global group
@@ -94,18 +104,18 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
if os.access(os.path.dirname(path), os.W_OK):
async with ClientSession() as client:
res = await client.get(url, ssl=get_ssl_context())
if res.status == 200:
data = BytesIO(await res.read())
remoteHash = sha256(data.getbuffer()).hexdigest()
if binHash == remoteHash:
data.seek(0)
with open(path, 'wb') as f:
f.write(data.getbuffer())
rv = True
else:
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
if res.status == 200:
data = BytesIO(await res.read())
remoteHash = sha256(data.getbuffer()).hexdigest()
if binHash == remoteHash:
data.seek(0)
with open(path, 'wb') as f:
f.write(data.getbuffer())
rv = True
else:
rv = False
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
else:
rv = False
except:
rv = False
+141 -122
View File
@@ -5,8 +5,8 @@ from logging import getLogger
from traceback import format_exc
from typing import List
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from aiohttp import ClientSession, WSMsgType
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
from asyncio.exceptions import TimeoutError
import uuid
@@ -32,13 +32,16 @@ class Tab:
self.websocket = await self.client.ws_connect(self.ws_url)
async def close_websocket(self):
await self.websocket.close()
await self.client.close()
async def listen_for_message(self):
async for message in self.websocket:
data = message.json()
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):
if self.websocket:
self.cmd_id += 1
@@ -52,20 +55,22 @@ class Tab:
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
if manage_socket:
await self.open_websocket()
try:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": run_async
}
}, get_result)
res = await self._send_devtools_cmd({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": run_async
}
}, get_result)
if manage_socket:
await self.close_websocket()
finally:
if manage_socket:
await self.close_websocket()
return res
async def has_global_var(self, var_name, manage_socket=True):
@@ -77,15 +82,17 @@ class Tab:
return res["result"]["result"]["value"]
async def close(self, manage_socket=True):
if manage_socket:
await self.open_websocket()
try:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.close",
}, False)
res = await self._send_devtools_cmd({
"method": "Page.close",
}, False)
if manage_socket:
await self.close_websocket()
finally:
if manage_socket:
await self.close_websocket()
return res
async def enable(self):
@@ -105,78 +112,82 @@ class Tab:
}, False)
async def refresh(self):
if manage_socket:
await self.open_websocket()
try:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({
"method": "Page.reload",
}, False)
await self._send_devtools_cmd({
"method": "Page.reload",
}, False)
if manage_socket:
await self.close_websocket()
finally:
if manage_socket:
await self.close_websocket()
return
async def reload_and_evaluate(self, js, manage_socket=True):
"""
Reloads the current tab, with JS to run on load via debugger
"""
if manage_socket:
await self.open_websocket()
try:
if manage_socket:
await self.open_websocket()
await self._send_devtools_cmd({
"method": "Debugger.enable"
}, True)
await self._send_devtools_cmd({
"method": "Debugger.enable"
}, 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({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"expression": "location.reload();",
"userGesture": True,
"awaitPromise": False
}
}, False)
await self._send_devtools_cmd({
"method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": breakpoint_res["result"]["breakpointId"]
}
}, 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({
"method": "Runtime.evaluate",
"params": {
"expression": js,
"userGesture": True,
"awaitPromise": False
}
}, False)
for x in range(4):
await self._send_devtools_cmd({
"method": "Debugger.resume"
"method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": breakpoint_res["result"]["breakpointId"]
}
}, False)
await self._send_devtools_cmd({
"method": "Debugger.disable"
}, True)
for x in range(4):
await self._send_devtools_cmd({
"method": "Debugger.resume"
}, False)
if manage_socket:
await self.close_websocket()
await self._send_devtools_cmd({
"method": "Debugger.disable"
}, True)
finally:
if manage_socket:
await self.close_websocket()
return
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)
None is returned if `get_result` is False
"""
try:
wrappedjs = """
function scriptFunc() {
{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
wrappedjs = """
function scriptFunc() {
{js}
}
}, get_result)
if (document.readyState === 'loading') {
addEventListener('DOMContentLoaded', () => {
scriptFunc();
});
} else {
scriptFunc();
}
""".format(js=js) if add_dom_wrapper else js
if manage_socket:
await self.close_websocket()
if manage_socket:
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
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`)
"""
if manage_socket:
await self.open_websocket()
try:
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {
"identifier": script_id
}
}, False)
res = await self._send_devtools_cmd({
"method": "Page.removeScriptToEvaluateOnNewDocument",
"params": {
"identifier": script_id
}
}, False)
if manage_socket:
await self.close_websocket()
finally:
if manage_socket:
await self.close_websocket()
async def has_element(self, element_name, manage_socket=True):
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 with ClientSession() as web:
res = {}
res = {}
while True:
try:
na = False
while True:
try:
async with ClientSession() as web:
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
except ClientConnectorError:
logger.debug("ClientConnectorError excepted.")
except ClientConnectorError:
if not na:
logger.debug("Steam isn't available yet. Wait for a moment...")
logger.error(format_exc())
await sleep(5)
except TimeoutError:
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
await sleep(1)
else:
break
if res.status == 200:
r = await res.json()
return [Tab(i) for i in r]
na = True
await sleep(5)
except ClientOSError:
logger.warn(f"The request to {BASE_ADDRESS}/json was reset")
await sleep(1)
except TimeoutError:
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
await sleep(1)
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:
+1 -1
View File
@@ -208,7 +208,7 @@ class Loader:
return web.Response(text=ret)
async def get_steam_resource(self, request):
tab = await get_tab("QuickAccess")
tab = await get_tab("SP")
try:
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:
+75 -45
View File
@@ -4,15 +4,15 @@ from subprocess import call
if hasattr(sys, '_MEIPASS'):
call(['chmod', '-R', '755', sys._MEIPASS])
# 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 logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, chmod, path
from os import getenv, chmod, listdir, path
from traceback import format_exc
import aiohttp_cors
# Partial imports
from aiohttp import ClientSession, client_exceptions
from aiohttp import client_exceptions, WSMsgType
from aiohttp.web import Application, Response, get, run_app, static
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,
get_home_path, get_homebrew_path, get_user,
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 loader import Loader
from settings import SettingsManager
@@ -56,15 +56,15 @@ basicConfig(
logger = getLogger("Main")
async def chown_plugin_dir(_):
async def chown_plugin_dir():
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
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})")
class PluginManager:
def __init__(self) -> None:
self.loop = get_event_loop()
def __init__(self, loop) -> None:
self.loop = loop
self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={
@@ -81,12 +81,20 @@ class PluginManager:
self.updater = Updater(self)
jinja_setup(self.web_app)
if CONFIG["chown_plugin_path"] == True:
self.web_app.on_startup.append(chown_plugin_dir)
self.loop.create_task(self.loader_reinjector())
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))
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:
chown_plugin_dir()
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
self.loop.create_task(self.reload_plugin_backends())
self.web_app.on_startup.append(startup)
self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
@@ -103,46 +111,66 @@ class PluginManager:
async def get_auth_token(self, request):
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):
await self.wait_for_server()
# await self.wait_for_server()
logger.debug("Loading plugins")
self.plugin_loader.import_plugins()
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def reload_plugin_backends(self, name):
await self.wait_for_server()
if name in self.plugin_loader.plugins:
self.plugin_loader.plugins[name].stop()
self.plugin_loader.plugins.pop(name, None)
else:
logger.error("Couldn't find plugin %s to reload.", str(name))
directories = [i for i in listdir(self.plugin_loader.plugin_path) if path.isdir(path.join(self.plugin_loader.plugin_path, i)) and path.isfile(path.join(self.plugin_loader.plugin_path, i, "plugin.json"))]
if name in directories:
Loader.import_plugin(path.join(self.plugin_loader.plugin_path, directories[name], "main.py"), directories[name], False, False)
async def loader_reinjector(self):
while True:
tab = None
nf = False
dc = False
while not tab:
try:
tab = await get_gamepadui_tab()
except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError:
logger.debug("Couldn't connect to debugger, waiting 5 seconds.")
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
if not dc:
logger.debug("Couldn't connect to debugger, waiting...")
dc = True
pass
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
if not tab:
await sleep(5)
await tab.open_websocket()
await tab.enable()
await self.inject_javascript(tab, True)
async for msg in tab.listen_for_message():
logger.debug("Page event: " + str(msg.get("method", None)))
if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab)
if msg.get("method", None) == "Inspector.detached":
logger.info("Steam is exiting...")
await tab.close_websocket()
break
try:
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)))
if msg.get("method", None) == "Page.domContentEventFired":
if not await tab.has_global_var("deckyHasLoaded", False):
await self.inject_javascript(tab)
if msg.get("method", None) == "Inspector.detached":
logger.info("CEF has requested that we detach.")
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:
logger.error("Exception while reading page events " + format_exc())
await tab.close_websocket()
pass
# while True:
# await sleep(5)
# if not await tab.has_global_var("deckyHasLoaded", False):
@@ -152,15 +180,15 @@ class PluginManager:
async def inject_javascript(self, tab: Tab, first=False, request=None):
logger.info("Loading Decky frontend!")
try:
if first:
if await tab.has_global_var("deckyHasLoaded", False):
tabs = await get_tabs()
for t in tabs:
if t.title != "Steam" and t.title != "SP":
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
await sleep(0.5)
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 1000)}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)
# if first:
# if await tab.has_global_var("deckyHasLoaded", False):
# tabs = await get_tabs()
# for t in tabs:
# if t.title != "Steam" and t.title != "SP":
# logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
# await t.close()
# await sleep(0.5)
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:
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass
@@ -169,4 +197,6 @@ class PluginManager:
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
if __name__ == "__main__":
PluginManager().run()
loop = new_event_loop()
set_event_loop(loop)
PluginManager(loop).run()
+20 -4
View File
@@ -1,20 +1,36 @@
import imp
from json import dump, load
from os import mkdir, path
from os import mkdir, path, listdir, rename
from shutil import chown
from helpers import get_home_path, get_homebrew_path, get_user, set_user
from helpers import get_home_path, get_homebrew_path, get_user, set_user, get_user_owner
class SettingsManager:
def __init__(self, name, settings_directory = None) -> None:
set_user()
USER = get_user()
wrong_dir = get_homebrew_path(get_home_path(USER))
if settings_directory == None:
settings_directory = get_homebrew_path(get_home_path(USER))
settings_directory = path.join(wrong_dir, "settings")
self.path = path.join(settings_directory, name + ".json")
#Create the folder with the correct permission
if not path.exists(settings_directory):
mkdir(settings_directory)
chown(settings_directory, USER, USER)
#Copy all old settings file in the root directory to the correct folder
for file in listdir(wrong_dir):
if file.endswith(".json"):
rename(path.join(wrong_dir,file),
path.join(settings_directory, file))
self.path = path.join(settings_directory, name + ".json")
#If the owner of the settings directory is not the user, then set it as the user:
if get_user_owner(settings_directory) != USER:
chown(settings_directory, USER, USER)
self.settings = {}
+36 -36
View File
@@ -87,12 +87,12 @@ class Updater:
branch = self.get_branch(self.context.settings)
match branch:
case 0:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-release.service"
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
case 1 | 2:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service"
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
case _:
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service"
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version(self):
@@ -112,20 +112,20 @@ class Updater:
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:
remoteVersions = await res.json()
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
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)
elif selectedBranch == 1:
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)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
tab = await get_gamepadui_tab()
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
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)
elif selectedBranch == 1:
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)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
tab = await get_gamepadui_tab()
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
return await self.get_version()
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:
logger.debug("Downloading service file")
data = await res.content.read()
logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service")
try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data)
except Exception as e:
logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file:
service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file:
service_file.write(service_data)
logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service")
try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data)
except Exception as e:
logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file:
service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file:
service_file.write(service_data)
logger.debug("Saved service 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))
progress = new_progress
with open(path.join(getcwd(), ".loader.version"), "w") as out:
out.write(version)
with open(path.join(getcwd(), ".loader.version"), "w") as out:
out.write(version)
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await self.do_restart()
await tab.client.close()
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await self.do_restart()
await tab.close_websocket()
async def do_restart(self):
call(["systemctl", "daemon-reload"])
+19 -18
View File
@@ -80,12 +80,13 @@ class Utilities:
async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web:
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
return {
"status": res.status,
"headers": dict(res.headers),
"body": await res.text()
}
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
text = await res.text()
return {
"status": res.status,
"headers": dict(res.headers),
"body": text
}
async def ping(self, **kwargs):
return "pong"
@@ -241,17 +242,17 @@ class Utilities:
if ip != None:
self.logger.info("Connecting to React DevTools at " + ip)
async with ClientSession() as web:
async with web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) as res:
if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip)
return False
self.start_rdt_proxy(ip, 8097)
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work.
result = await tab.reload_and_evaluate(script)
self.logger.info(result)
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip)
return False
self.start_rdt_proxy(ip, 8097)
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work.
result = await tab.reload_and_evaluate(script)
self.logger.info(result)
except Exception:
self.logger.error("Failed to connect to React DevTools")
@@ -261,5 +262,5 @@ class Utilities:
self.logger.info("Disabling React DevTools")
tab = await get_gamepadui_tab()
self.rdt_script_id = None
await tab.evaluate_js("location.reload();", False, True, False)
await tab.evaluate_js("SteamClient.User.StartRestart();", False, True, False)
self.logger.info("React DevTools disabled")
+2 -1
View File
@@ -27,7 +27,7 @@ systemctl --user disable plugin_loader 2> /dev/null
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
[Unit]
@@ -40,6 +40,7 @@ User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=DEBUG
[Install]
+2 -1
View File
@@ -27,7 +27,7 @@ systemctl --user disable plugin_loader 2> /dev/null
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
[Unit]
@@ -40,6 +40,7 @@ User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=INFO
[Install]
+2 -1
View File
@@ -8,7 +8,8 @@ User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=DEBUG
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
+2 -1
View File
@@ -8,7 +8,8 @@ User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=INFO
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
+1 -1
View File
@@ -41,7 +41,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^3.7.12",
"decky-frontend-lib": "^3.7.14",
"react-file-icon": "^1.2.0",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
+4 -4
View File
@@ -10,7 +10,7 @@ specifiers:
'@types/react-file-icon': ^1.0.1
'@types/react-router': 5.1.18
'@types/webpack': ^5.28.0
decky-frontend-lib: ^3.7.12
decky-frontend-lib: ^3.7.14
husky: ^8.0.1
import-sort-style-module: ^6.0.0
inquirer: ^8.2.4
@@ -30,7 +30,7 @@ specifiers:
typescript: ^4.7.4
dependencies:
decky-frontend-lib: 3.7.12
decky-frontend-lib: 3.7.14
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
react-icons: 4.4.0_react@16.14.0
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
@@ -944,8 +944,8 @@ packages:
dependencies:
ms: 2.1.2
/decky-frontend-lib/3.7.12:
resolution: {integrity: sha512-whDV9zHuEBFj17zKoT51aRcUxLvSzBNu2lc242/EO9aFFP064FVCrJu+r7CxWe0hlQ7sA4FKX1qgCwsZ6H+PZg==}
/decky-frontend-lib/3.7.14:
resolution: {integrity: sha512-ShAoP3VqiwkJYukDBHsOF9fk7wYw0VaKpHw6j9WdzLxwZwBcg0J7QBNIFYP3nfA0UgEwSJVEg/22kONiplipmA==}
dev: false
/decode-named-character-reference/1.0.2:
+9
View File
@@ -7,6 +7,11 @@ import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
const hiddenWarnings = [
"THIS_IS_UNDEFINED",
"EVAL"
];
export default defineConfig({
input: 'src/index.tsx',
plugins: [
@@ -35,5 +40,9 @@ export default defineConfig({
chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js'
}
},
onwarn: function ( message, handleWarning ) {
if (hiddenWarnings.some(warning => message.code === warning)) return;
handleWarning(message);
}
});
@@ -5,7 +5,7 @@ import { FaShapes, FaTools } from 'react-icons/fa';
import { installFromURL } from '../../../../store';
import BranchSelect from './BranchSelect';
import RemoteDebuggingSettings from './RemoteDebugging';
// import StoreSelect from './StoreSelect';
import StoreSelect from './StoreSelect';
import UpdaterSettings from './Updater';
export default function GeneralSettings({
@@ -21,7 +21,7 @@ export default function GeneralSettings({
<div>
<UpdaterSettings />
<BranchSelect />
{/* <StoreSelect /> */}
<StoreSelect />
<RemoteDebuggingSettings />
<Field
label="Developer mode"
+20 -2
View File
@@ -8,8 +8,8 @@ import {
sleep,
staticClasses,
} from 'decky-frontend-lib';
import { lazy } from 'react';
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
import { FC, lazy } from 'react';
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
@@ -245,6 +245,24 @@ class PluginLoader extends Logger {
});
} catch (e) {
this.error('Error loading plugin ' + name, e);
const TheError: FC<{}> = () => (
<>
Error:{' '}
<pre>
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
</pre>
<>
Please go to <FaCog style={{ display: 'inline' }} /> in the Decky menu if you need to uninstall this
plugin.
</>
</>
);
this.plugins.push({
name: name,
version: version,
content: <TheError />,
icon: <FaExclamationCircle />,
});
this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, icon: <FaExclamationCircle /> });
}
} else throw new Error(`${name} frontend_bundle not OK`);
+40 -42
View File
@@ -1,4 +1,5 @@
import { Plugin } from './plugin';
import { getSetting, setSetting } from './utils/settings';
export enum Store {
Default,
@@ -9,6 +10,7 @@ export enum Store {
export interface StorePluginVersion {
name: string;
hash: string;
artifact: string | undefined | null;
}
export interface StorePlugin {
@@ -26,47 +28,41 @@ export type PluginUpdateMapping = Map<string, StorePluginVersion>;
export async function getPluginList(): Promise<StorePlugin[]> {
let version = await window.DeckyPluginLoader.updateVersion();
return fetch('https://plugins.deckbrew.xyz/plugins', {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
}).then((r) => r.json());
// let store = await getSetting<Store>('store', Store.Default);
// let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
// let storeURL;
// if (!store) {
// console.log('Could not get a default store, using Default.');
// await setSetting('store-url', Store.Default);
// return fetch('https://plugins.deckbrew.xyz/plugins', {
// method: 'GET',
// headers: {
// 'X-Decky-Version': version.current,
// },
// }).then((r) => r.json());
// } else {
// switch (+store) {
// case Store.Default:
// storeURL = 'https://plugins.deckbrew.xyz/plugins';
// break;
// case Store.Testing:
// storeURL = 'https://testing.deckbrew.xyz/plugins';
// break;
// case Store.Custom:
// storeURL = customURL;
// break;
// default:
// console.error('Somehow you ended up without a standard URL, using the default URL.');
// storeURL = 'https://plugins.deckbrew.xyz/plugins';
// break;
// }
// return fetch(storeURL, {
// method: 'GET',
// headers: {
// 'X-Decky-Version': version.current,
// },
// }).then((r) => r.json());
// }
let store = await getSetting<Store>('store', Store.Default);
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
let storeURL;
if (!store) {
console.log('Could not get a default store, using Default.');
await setSetting('store-url', Store.Default);
return fetch('https://plugins.deckbrew.xyz/plugins', {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
}).then((r) => r.json());
} else {
switch (+store) {
case Store.Default:
storeURL = 'https://plugins.deckbrew.xyz/plugins';
break;
case Store.Testing:
storeURL = 'https://testing.deckbrew.xyz/plugins';
break;
case Store.Custom:
storeURL = customURL;
break;
default:
console.error('Somehow you ended up without a standard URL, using the default URL.');
storeURL = 'https://plugins.deckbrew.xyz/plugins';
break;
}
return fetch(storeURL, {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
}).then((r) => r.json());
}
}
export async function installFromURL(url: string) {
@@ -78,9 +74,11 @@ export async function installFromURL(url: string) {
}
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
const artifactUrl =
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
name: plugin,
artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`,
artifact: artifactUrl,
version: selectedVer.name,
hash: selectedVer.hash,
});