Compare commits

..

14 Commits

Author SHA1 Message Date
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
17 changed files with 443 additions and 295 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.]
+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": []
}
]
}
}
+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:
+62 -44
View File
@@ -4,7 +4,7 @@ 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
@@ -12,7 +12,7 @@ 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,19 @@ 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.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 +110,55 @@ 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 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 +168,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 +185,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"])
+18 -18
View File
@@ -80,12 +80,12 @@ 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)
return {
"status": res.status,
"headers": dict(res.headers),
"body": await res.text()
}
async def ping(self, **kwargs):
return "pong"
@@ -241,17 +241,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 +261,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")
+1 -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]
+1 -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]
+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:
+10 -1
View File
@@ -5,7 +5,12 @@ import externalGlobals from "rollup-plugin-external-globals";
import del from 'rollup-plugin-delete'
import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
import { defineConfig, handleWarning } from 'rollup';
const hiddenWarnings = [
"THIS_IS_UNDEFINED",
"EVAL"
];
export default defineConfig({
input: 'src/index.tsx',
@@ -35,5 +40,9 @@ export default defineConfig({
chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js'
}
},
onwarn: function ( message ) {
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`);
+36 -41
View File
@@ -1,4 +1,5 @@
import { Plugin } from './plugin';
import { getSetting, setSetting } from './utils/settings';
export enum Store {
Default,
@@ -26,47 +27,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) {