Compare commits

...

13 Commits

Author SHA1 Message Date
AAGaming e6cc4bba5c hotfix: change store URL in service file 2022-06-28 13:01:21 -04:00
Liam Dawe 1199c080bc Update README.md, password is needed (#70)
* Update README.md

There is no password by default, so people need to set one before running that script.

* Update README.md

add the guide for password
2022-06-06 14:35:56 -07:00
Jonas Dellinger 414e0da2f3 Fix hot-reload when there are subdirs (#56) 2022-05-11 02:11:14 +03:00
tza cb9b888dc6 Merge branch 'main' of https://github.com/SteamDeckHomebrew/PluginLoader 2022-05-10 23:17:12 +03:00
tza f3ab0f5989 Plugin store button now uses built-in browser 2022-05-10 23:17:09 +03:00
marios e132aba0f8 Fixed race condition pr 2022-05-10 21:11:51 +03:00
tza 0d0e57e35a Added store button 2022-05-10 20:31:39 +03:00
Patrick Kubiak 945db5de47 Use unique ids in call_server_method (#55) 2022-05-10 17:13:53 +03:00
marios 28746e3962 Update README.md 2022-05-04 11:43:28 +03:00
Gabriel Jones 279b1e8c40 Uninstall script addition (#48)
* Create uninstall.sh

* Update uninstall.sh

First pass for a version that looks similar to the install scripts

* Update readme with uninstall info

Add uninstall script info to readme

* Update dist/uninstall.sh

Only remove services for now

Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>

* Adjust echo to accurately reflect script actions

No longer deletes installed plugins, adjusted echo to match actions.

Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>
2022-05-04 11:42:03 +03:00
marios 89ecca7c30 Fixed callsign debug bug, Fixed process spawn and termination bug 2022-04-29 21:51:01 +03:00
marios 7d74e98f4f Bug fixes
- Fixed KeyError in execute_in_tab
- Changed the plugin process dispatch method, this *should* fix that annoying server hang issue.
2022-04-29 12:52:24 +03:00
marios fe1f6473e9 method call listener retry bug fix, method call response serializaiton failure fix,
- Added retry logic to the QuickAccess tab fetching in the method call listener.
- Added exception handling, in case a plugin method returns something that can't be serialized as JSON.
- Changed a few log calls from info to debug to prevent spam
- Added a filter for asyncio base_event log records, since they get spamy and don't provide any useful info most of the time. This can be turned off with the LOG_BASE_EVENTS envar.
2022-04-26 23:37:01 +03:00
12 changed files with 138 additions and 44 deletions
+9 -2
View File
@@ -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
+1
View File
@@ -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]
+1
View File
@@ -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
+17
View File
@@ -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
+2 -2
View File
@@ -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):
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+6 -5
View File
@@ -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 -1
View File
@@ -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};
+25 -2
View File
@@ -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"/>
+22 -9
View File
@@ -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")
})();
""")