mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6cc4bba5c | |||
| 1199c080bc | |||
| 414e0da2f3 | |||
| cb9b888dc6 | |||
| f3ab0f5989 | |||
| e132aba0f8 | |||
| 0d0e57e35a | |||
| 945db5de47 | |||
| 28746e3962 | |||
| 279b1e8c40 | |||
| 89ecca7c30 | |||
| 7d74e98f4f | |||
| fe1f6473e9 |
@@ -8,16 +8,23 @@
|
||||
3. Scroll the sidebar all the way down and click on `Developer`
|
||||
4. Under Miscellaneous, enable `CEF Remote Debugging`
|
||||
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
|
||||
6. Open a terminal and paste the following command into it:
|
||||
6. Make sure you have a password set with the "passwd" command in terminal to install it ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)).
|
||||
7. Open a terminal and paste the following command into it:
|
||||
- For users:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_release.sh | sh`
|
||||
- For developers:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`
|
||||
~~- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`~~
|
||||
Nightly releases are currently broken.
|
||||
8. Done! Reboot back into Gaming mode and enjoy your plugins!
|
||||
|
||||
### Install Plugins
|
||||
- Simply copy the plugin's folder into `~/homebrew/plugins`
|
||||
|
||||
### Uninstall
|
||||
- Open a terminal and paste the following command into it:
|
||||
- For both users and developers:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
|
||||
|
||||
### Developing plugins
|
||||
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/Plugin-Template) repository
|
||||
|
||||
|
||||
Vendored
+1
@@ -40,6 +40,7 @@ Restart=always
|
||||
ExecStart=/home/deck/homebrew/services/PluginLoader
|
||||
WorkingDirectory=/home/deck/homebrew/services
|
||||
|
||||
Environment=STORE_URL=https://plugins.deckbrew.xyz
|
||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
||||
|
||||
[Install]
|
||||
|
||||
Vendored
+1
@@ -30,6 +30,7 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=/home/deck/homebrew/services/PluginLoader
|
||||
WorkingDirectory=/home/deck/homebrew/services
|
||||
Environment=STORE_URL=https://plugins.deckbrew.xyz
|
||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Uninstalling Steam Deck Plugin Loader..."
|
||||
|
||||
HOMEBREW_FOLDER=/home/deck/homebrew
|
||||
|
||||
# Disable and remove services
|
||||
sudo systemctl disable --now plugin_loader.service > /dev/null
|
||||
sudo rm -f /home/deck/.config/systemd/user/plugin_loader.service
|
||||
sudo rm -f /etc/systemd/system/plugin_loader.service
|
||||
|
||||
# Remove temporary folder if it exists from the install process
|
||||
rm -rf /tmp/plugin_loader
|
||||
|
||||
# Cleanup services folder
|
||||
sudo rm ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from injector import get_tab
|
||||
from injector import get_tab, inject_to_tab
|
||||
from logging import getLogger
|
||||
from os import path, rename
|
||||
from shutil import rmtree
|
||||
@@ -26,7 +26,7 @@ class PluginBrowser:
|
||||
|
||||
server_instance.add_routes([
|
||||
web.post("/browser/install_plugin", self.install_plugin),
|
||||
web.get("/browser/iframe", self.redirect_to_store)
|
||||
web.get("/browser/redirect", self.redirect_to_store),
|
||||
])
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from aiohttp import ClientSession
|
||||
from logging import debug, getLogger
|
||||
from asyncio import sleep
|
||||
from traceback import format_exc
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
@@ -61,14 +62,15 @@ async def get_tabs():
|
||||
res = await web.get(f"{BASE_ADDRESS}/json")
|
||||
break
|
||||
except:
|
||||
logger.info("Steam isn't available yet. Wait for a moment...")
|
||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||
logger.debug(format_exc())
|
||||
await sleep(5)
|
||||
|
||||
if res.status == 200:
|
||||
res = await res.json()
|
||||
return [Tab(i) for i in res]
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
raise Exception(f"/json did not return 200. {await r.text()}")
|
||||
|
||||
async def get_tab(tab_name):
|
||||
tabs = await get_tabs()
|
||||
|
||||
+14
-12
@@ -6,6 +6,8 @@ from asyncio import Queue
|
||||
from os import path, listdir
|
||||
from logging import getLogger
|
||||
from time import time
|
||||
from genericpath import exists
|
||||
from pathlib import Path
|
||||
|
||||
from injector import get_tabs, get_tab
|
||||
from plugin import PluginWrapper
|
||||
@@ -18,6 +20,12 @@ class FileChangeHandler(FileSystemEventHandler):
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
|
||||
def maybe_reload(self, src_path):
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
self.logger.info(path.join(self.plugin_path, plugin_dir, "plugin.json"))
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
@@ -27,13 +35,8 @@ class FileChangeHandler(FileSystemEventHandler):
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
rel_path = path.relpath(src_path, path.commonprefix([self.plugin_path, src_path]))
|
||||
plugin_dir = path.split(rel_path)[0]
|
||||
main_file_path = path.join(self.plugin_path, plugin_dir, "main.py")
|
||||
self.queue.put_nowait((main_file_path, plugin_dir, True))
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
src_path = event.src_path
|
||||
@@ -44,11 +47,8 @@ class FileChangeHandler(FileSystemEventHandler):
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file modified: {src_path}")
|
||||
plugin_dir = path.split(path.relpath(src_path, path.commonprefix([self.plugin_path, src_path])))[0]
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||
@@ -58,6 +58,7 @@ class Loader:
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.callsigns = {}
|
||||
self.callsign_matches = {}
|
||||
self.import_plugins()
|
||||
|
||||
if live_reload:
|
||||
@@ -85,13 +86,14 @@ class Loader:
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
self.callsigns.pop(plugin.callsign, None)
|
||||
self.callsigns.pop(self.callsign_matches[file], None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
callsign = str(time())
|
||||
plugin.callsign = callsign
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.callsigns[callsign] = plugin
|
||||
self.callsign_matches[file] = callsign
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
@@ -119,7 +121,7 @@ class Loader:
|
||||
return await self.callsigns[callsign].execute_method(method_name, kwargs)
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
tab = (await get_tabs())[0]
|
||||
tab = await get_tab("QuickAccess")
|
||||
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:
|
||||
|
||||
+27
-7
@@ -1,4 +1,4 @@
|
||||
from logging import getLogger, basicConfig, INFO, DEBUG
|
||||
from logging import getLogger, basicConfig, INFO, DEBUG, Filter, root
|
||||
from os import getenv
|
||||
|
||||
CONFIG = {
|
||||
@@ -7,10 +7,18 @@ CONFIG = {
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
|
||||
"store_url": getenv("STORE_URL", "https://sdh.tzatzi.me/browse")
|
||||
"store_url": getenv("STORE_URL", "https://beta.deckbrew.xyz"),
|
||||
"log_base_events": getenv("LOG_BASE_EVENTS", "0")=="1"
|
||||
}
|
||||
|
||||
class NoBaseEvents(Filter):
|
||||
def filter(self, record):
|
||||
return not "asyncio" in record.name
|
||||
|
||||
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
||||
for handler in root.handlers:
|
||||
if not CONFIG["log_base_events"]:
|
||||
handler.addFilter(NoBaseEvents())
|
||||
|
||||
from aiohttp.web import Application, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
@@ -26,6 +34,7 @@ from utilities import Utilities
|
||||
from browser import PluginBrowser
|
||||
|
||||
logger = getLogger("Main")
|
||||
from traceback import print_exc
|
||||
|
||||
async def chown_plugin_dir(_):
|
||||
Popen(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
|
||||
@@ -55,11 +64,11 @@ class PluginManager:
|
||||
|
||||
async def loader_reinjector(self):
|
||||
finished_reinjection = False
|
||||
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
while True:
|
||||
await sleep(1)
|
||||
if not await tab_has_element("QuickAccess", "plugin_iframe"):
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
logger.debug("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
await self.inject_javascript()
|
||||
finished_reinjection = True
|
||||
elif finished_reinjection:
|
||||
@@ -77,11 +86,17 @@ class PluginManager:
|
||||
pass
|
||||
|
||||
async def resolve_method_call(self, tab, call_id, response):
|
||||
try:
|
||||
r = dumps(response)
|
||||
except Exception as e:
|
||||
logger.error(response["result"])
|
||||
response["result"] = str(response["result"])
|
||||
r = response
|
||||
await tab._send_devtools_cmd({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": f"resolveMethodCall({call_id}, {dumps(response)})",
|
||||
"expression": f"resolveMethodCall('{call_id}', {r})",
|
||||
"userGesture": True
|
||||
}
|
||||
}, receive=False)
|
||||
@@ -107,7 +122,12 @@ class PluginManager:
|
||||
await self.resolve_method_call(tab, method["id"], res)
|
||||
|
||||
async def method_call_listener(self):
|
||||
tab = await get_tab("QuickAccess")
|
||||
while True:
|
||||
try:
|
||||
tab = await get_tab("QuickAccess")
|
||||
break
|
||||
except:
|
||||
await sleep(1)
|
||||
await tab.open_websocket()
|
||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"})
|
||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"})
|
||||
@@ -121,4 +141,4 @@ 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()
|
||||
PluginManager().run()
|
||||
|
||||
@@ -2,8 +2,10 @@ from importlib.util import spec_from_file_location, module_from_spec
|
||||
from asyncio import get_event_loop, new_event_loop, set_event_loop, start_unix_server, open_unix_connection, sleep, Lock
|
||||
from os import path, setuid
|
||||
from json import loads, dumps, load
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from time import time
|
||||
from multiprocessing import Process
|
||||
from signal import signal, SIGINT
|
||||
from sys import exit
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
@@ -25,6 +27,8 @@ class PluginWrapper:
|
||||
self.passive = not path.isfile(self.file)
|
||||
|
||||
def _init(self):
|
||||
signal(SIGINT, lambda s, f: exit(0))
|
||||
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
@@ -73,10 +77,7 @@ class PluginWrapper:
|
||||
def start(self):
|
||||
if self.passive:
|
||||
return self
|
||||
get_event_loop().run_in_executor(
|
||||
ProcessPoolExecutor(),
|
||||
self._init
|
||||
)
|
||||
Process(target=self._init).start()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ window.addEventListener("message", function(evt) {
|
||||
}, false);
|
||||
|
||||
async function call_server_method(method_name, arg_object={}) {
|
||||
let id = `${new Date().getTime()}`;
|
||||
let id = `${uuidv4()}`;
|
||||
console.debug(JSON.stringify({
|
||||
"id": id,
|
||||
"method": method_name,
|
||||
@@ -22,6 +22,13 @@ async function call_server_method(method_name, arg_object={}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/2117523 Thanks!
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
async function fetch_nocors(url, request={}) {
|
||||
let args = { method: "POST", headers: {}, body: "" };
|
||||
request = {...args, ...request};
|
||||
|
||||
@@ -53,8 +53,22 @@ function addPluginInstallPrompt(artifact, version, request_id) {
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const SHOP_ICON = `
|
||||
<button
|
||||
class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
|
||||
style="width: auto; padding-left: 10px; padding-right: 10px; margin-right: 1rem; margin-left: auto; padding-top: 3px;"
|
||||
id="open_shop_button"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bag-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2.5 2.5 0 0 1 2.5 2.5V4h-5v-.5A2.5 2.5 0 0 1 8 1zm3.5 3v-.5a3.5 3.5 0 1 0-7 0V4H1v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4h-3.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
`
|
||||
|
||||
function createTitle(text) {
|
||||
return `<div id="plugin_title" class="quickaccessmenu_Title_34nl5">${text}</div>`;
|
||||
return `
|
||||
<div class="quickaccessmenu_Title_34nl5"><div id="plugin_title">${text}</div>${SHOP_ICON}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createPluginList() {
|
||||
@@ -79,11 +93,20 @@ function addPluginInstallPrompt(artifact, version, request_id) {
|
||||
inject();
|
||||
document.getElementById("plugin_title").onclick = function() {
|
||||
reloadIframe();
|
||||
document.getElementById("plugin_title").innerText = "Plugins";
|
||||
document.getElementById("plugin_title").innerText = `Plugins`;
|
||||
document.getElementById("open_shop_button").style.display = 'block';
|
||||
}
|
||||
document.getElementById("open_shop_button").onclick = function(ev) {
|
||||
console.debug(JSON.stringify({
|
||||
"id": 1,
|
||||
"method": "open_plugin_store",
|
||||
"args": {}
|
||||
}));
|
||||
}
|
||||
window.onmessage = function(ev) {
|
||||
let title = ev.data;
|
||||
if (title.startsWith("PLUGIN_LOADER__")) {
|
||||
document.getElementById("open_shop_button").style.display = 'none';
|
||||
document.getElementById("plugin_title").innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12zm-4.5-6.5H5.707l2.147-2.146a.5.5 0 1 0-.708-.708l-3 3a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708-.708L5.707 8.5H11.5a.5.5 0 0 0 0-1z"/>
|
||||
|
||||
@@ -11,7 +11,8 @@ class Utilities:
|
||||
"confirm_plugin_install": self.confirm_plugin_install,
|
||||
"execute_in_tab": self.execute_in_tab,
|
||||
"inject_css_into_tab": self.inject_css_into_tab,
|
||||
"remove_css_from_tab": self.remove_css_from_tab
|
||||
"remove_css_from_tab": self.remove_css_from_tab,
|
||||
"open_plugin_store": self.open_plugin_store
|
||||
}
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
@@ -19,12 +20,12 @@ class Utilities:
|
||||
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, **kwargs)
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
}
|
||||
async with web.request(method, url, **kwargs) as res:
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
return "pong"
|
||||
@@ -32,7 +33,6 @@ class Utilities:
|
||||
async def execute_in_tab(self, tab, run_async, code):
|
||||
try:
|
||||
result = await inject_to_tab(tab, code, run_async)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
@@ -41,7 +41,7 @@ class Utilities:
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result" : result["result"]["result"]["value"]
|
||||
"result" : result["result"]["result"].get("value")
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
@@ -105,3 +105,16 @@ class Utilities:
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def open_plugin_store(self):
|
||||
await inject_to_tab("SP", """
|
||||
(function() {
|
||||
wpRequire = webpackJsonp.push([[], { get_require: (mod, _exports, wpRequire) => mod.exports = wpRequire }, [["get_require"]]]);
|
||||
const all = () => Object.keys(wpRequire.c).map((x) => wpRequire.c[x].exports).filter((x) => x);
|
||||
router = all().map(m => {
|
||||
if (typeof m !== "object") return undefined;
|
||||
for (let prop in m) { if (m[prop]?.Navigate) return m[prop]}
|
||||
}).find(x => x)
|
||||
router.NavigateToExternalWeb("http://127.0.0.1:1337/browser/redirect")
|
||||
})();
|
||||
""")
|
||||
Reference in New Issue
Block a user