summaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/browser.py78
-rw-r--r--backend/helpers.py30
-rw-r--r--backend/loader.py15
-rw-r--r--backend/main.py15
-rw-r--r--backend/plugin.py38
-rw-r--r--backend/updater.py36
-rw-r--r--backend/utilities.py30
7 files changed, 199 insertions, 43 deletions
diff --git a/backend/browser.py b/backend/browser.py
index 83c68d47..8e2209f1 100644
--- a/backend/browser.py
+++ b/backend/browser.py
@@ -3,19 +3,19 @@ import json
# Partial imports
from aiohttp import ClientSession, web
-from asyncio import get_event_loop
+from asyncio import get_event_loop, sleep
from concurrent.futures import ProcessPoolExecutor
from hashlib import sha256
from io import BytesIO
from logging import getLogger
-from os import path, rename, listdir
+from os import R_OK, W_OK, path, rename, listdir, access, mkdir
from shutil import rmtree
from subprocess import call
from time import time
from zipfile import ZipFile
# Local modules
-from helpers import get_ssl_context, get_user, get_user_group
+from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
from injector import get_tab, inject_to_tab
logger = getLogger("Browser")
@@ -28,9 +28,10 @@ class PluginInstallContext:
self.hash = hash
class PluginBrowser:
- def __init__(self, plugin_path, plugins) -> None:
+ def __init__(self, plugin_path, plugins, loader) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
+ self.loader = loader
self.install_requests = {}
def _unzip_to_plugin_dir(self, zip, name, hash):
@@ -39,12 +40,55 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
- code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
- code_chmod = call(["chmod", "-R", "555", self.plugin_path])
+ plugin_dir = self.find_plugin_folder(name)
+ code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
+ code_chmod = call(["chmod", "-R", "555", plugin_dir])
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})")
return False
return True
+
+ async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
+ rv = False
+ try:
+ packageJsonPath = path.join(pluginBasePath, 'package.json')
+ pluginBinPath = path.join(pluginBasePath, 'bin')
+
+ if access(packageJsonPath, R_OK):
+ with open(packageJsonPath, 'r') as f:
+ packageJson = json.load(f)
+ if len(packageJson["remote_binary"]) > 0:
+ # create bin directory if needed.
+ rc=call(["chmod", "-R", "777", pluginBasePath])
+ if access(pluginBasePath, W_OK):
+
+ if not path.exists(pluginBinPath):
+ mkdir(pluginBinPath)
+
+ if not access(pluginBinPath, W_OK):
+ rc=call(["chmod", "-R", "777", pluginBinPath])
+
+ rv = True
+ for remoteBinary in packageJson["remote_binary"]:
+ # Required Fields. If any Remote Binary is missing these fail the install.
+ binName = remoteBinary["name"]
+ binURL = remoteBinary["url"]
+ binHash = remoteBinary["sha256hash"]
+ if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
+ rv = False
+ raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
+
+ code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
+ rc=call(["chmod", "-R", "555", pluginBasePath])
+ else:
+ rv = True
+ logger.debug(f"No Remote Binaries to Download")
+
+ except Exception as e:
+ rv = False
+ logger.debug(str(e))
+
+ return rv
def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path):
@@ -58,6 +102,8 @@ class PluginBrowser:
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name):
+ if self.loader.watcher:
+ self.loader.watcher.disabled = True
tab = await get_tab("SP")
try:
logger.info("uninstalling " + name)
@@ -74,8 +120,12 @@ class PluginBrowser:
except Exception as e:
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
logger.error(f"Error at %s", exc_info=e)
+ if self.loader.watcher:
+ self.loader.watcher.disabled = False
async def _install(self, artifact, name, version, hash):
+ if self.loader.watcher:
+ self.loader.watcher.disabled = True
try:
await self.uninstall_plugin(name)
except:
@@ -92,10 +142,22 @@ class PluginBrowser:
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
- logger.info(f"Installed {name} (Version: {version})")
- await inject_to_tab("SP", "window.syncDeckyPlugins()")
+ plugin_dir = self.find_plugin_folder(name)
+ ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
+ if ret:
+ logger.info(f"Installed {name} (Version: {version})")
+ if name in self.loader.plugins:
+ self.loader.plugins[name].stop()
+ self.loader.plugins.pop(name, None)
+ await sleep(1)
+ self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
+ # await inject_to_tab("SP", "window.syncDeckyPlugins()")
+ else:
+ logger.fatal(f"Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
+ if self.loader.watcher:
+ self.loader.watcher.disabled = False
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
diff --git a/backend/helpers.py b/backend/helpers.py
index c54139cc..0b6e7746 100644
--- a/backend/helpers.py
+++ b/backend/helpers.py
@@ -2,11 +2,15 @@ import re
import ssl
import subprocess
import uuid
+import os
from subprocess import check_output
from time import sleep
+from hashlib import sha256
+from io import BytesIO
import certifi
from aiohttp.web import Response, middleware
+from aiohttp import ClientSession
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
@@ -27,7 +31,7 @@ def get_csrf_token():
@middleware
async def csrf_middleware(request, handler):
- if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
+ if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status='403')
@@ -83,6 +87,30 @@ def get_homebrew_path(home_path = None) -> str:
return str(home_path+"/homebrew")
# return str(home_path+"/homebrew")
+# Download Remote Binaries to local Plugin
+async def download_remote_binary_to_path(url, binHash, path) -> bool:
+ rv = False
+ try:
+ 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}")
+ else:
+ rv = False
+ except:
+ rv = False
+
+ return rv
+
async def is_systemd_unit_active(unit_name: str) -> bool:
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return res.returncode == 0
diff --git a/backend/loader.py b/backend/loader.py
index 9c2b33f4..b4559180 100644
--- a/backend/loader.py
+++ b/backend/loader.py
@@ -25,8 +25,11 @@ class FileChangeHandler(RegexMatchingEventHandler):
self.logger = getLogger("file-watcher")
self.plugin_path = plugin_path
self.queue = queue
+ self.disabled = True
def maybe_reload(self, src_path):
+ if self.disabled:
+ return
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
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))
@@ -66,13 +69,17 @@ class Loader:
self.plugin_path = plugin_path
self.logger.info(f"plugin_path: {self.plugin_path}")
self.plugins = {}
+ self.watcher = None
+ self.live_reload = live_reload
if live_reload:
self.reload_queue = Queue()
self.observer = Observer()
- self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True)
+ self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
+ self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
self.observer.start()
self.loop.create_task(self.handle_reloads())
+ self.loop.create_task(self.enable_reload_wait())
server_instance.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
@@ -87,6 +94,12 @@ class Loader:
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
])
+ async def enable_reload_wait(self):
+ if self.live_reload:
+ await sleep(10)
+ self.logger.info("Hot reload enabled")
+ self.watcher.disabled = False
+
async def handle_frontend_assets(self, request):
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
diff --git a/backend/main.py b/backend/main.py
index 83032be3..ab64a3d9 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,9 +1,14 @@
+# Change PyInstaller files permissions
+import sys
+from subprocess import call
+if hasattr(sys, '_MEIPASS'):
+ call(['chmod', '-R', '755', sys._MEIPASS])
# Full imports
from asyncio import get_event_loop, sleep
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
-from os import getenv, path
-from subprocess import call
+from os import getenv, chmod
+from traceback import format_exc
import aiohttp_cors
# Partial imports
@@ -70,7 +75,7 @@ class PluginManager:
)
})
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
- self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins)
+ self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader)
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
self.utilities = Utilities(self)
self.updater = Updater(self)
@@ -123,9 +128,9 @@ class PluginManager:
async def inject_javascript(self, request=None):
try:
- await inject_to_tab("SP", "try{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)}", True)
+ await inject_to_tab("SP", "try{if (window.deckyHasLoaded) location.reload();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)}", True)
except:
- logger.info("Failed to inject JavaScript into tab")
+ logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass
def run(self):
diff --git a/backend/plugin.py b/backend/plugin.py
index b16d40d8..9d9a22c6 100644
--- a/backend/plugin.py
+++ b/backend/plugin.py
@@ -5,6 +5,8 @@ from asyncio import (Lock, get_event_loop, new_event_loop,
from concurrent.futures import ProcessPoolExecutor
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads
+from logging import getLogger
+from traceback import format_exc
from os import path, setgid, setuid
from signal import SIGINT, signal
from sys import exit
@@ -40,28 +42,34 @@ class PluginWrapper:
self.author = json["author"]
self.flags = json["flags"]
+ self.log = getLogger("plugin")
+
self.passive = not path.isfile(self.file)
def __str__(self) -> str:
return self.name
def _init(self):
- signal(SIGINT, lambda s, f: exit(0))
+ try:
+ signal(SIGINT, lambda s, f: exit(0))
- set_event_loop(new_event_loop())
- if self.passive:
- return
- setgid(0 if "root" in self.flags else 1000)
- setuid(0 if "root" in self.flags else 1000)
- spec = spec_from_file_location("_", self.file)
- module = module_from_spec(spec)
- spec.loader.exec_module(module)
- self.Plugin = module.Plugin
-
- if hasattr(self.Plugin, "_main"):
- get_event_loop().create_task(self.Plugin._main(self.Plugin))
- get_event_loop().create_task(self._setup_socket())
- get_event_loop().run_forever()
+ set_event_loop(new_event_loop())
+ if self.passive:
+ return
+ setgid(0 if "root" in self.flags else 1000)
+ setuid(0 if "root" in self.flags else 1000)
+ spec = spec_from_file_location("_", self.file)
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+ self.Plugin = module.Plugin
+
+ if hasattr(self.Plugin, "_main"):
+ get_event_loop().create_task(self.Plugin._main(self.Plugin))
+ get_event_loop().create_task(self._setup_socket())
+ get_event_loop().run_forever()
+ except:
+ self.log.error("Failed to start " + self.name + "!\n" + format_exc())
+ exit(0)
async def _setup_socket(self):
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
diff --git a/backend/updater.py b/backend/updater.py
index ba62efd8..402c152b 100644
--- a/backend/updater.py
+++ b/backend/updater.py
@@ -20,7 +20,7 @@ class Updater:
self.settings = self.context.settings
# Exposes updater methods to frontend
self.updater_methods = {
- "get_branch": self.get_branch,
+ "get_branch": self._get_branch,
"get_version": self.get_version,
"do_update": self.do_update,
"do_restart": self.do_restart,
@@ -29,18 +29,18 @@ class Updater:
self.remoteVer = None
self.allRemoteVers = None
try:
- self.currentBranch = self.get_branch(self.context.settings)
- if int(self.currentBranch) == -1:
- raise ValueError("get_branch could not determine branch!")
- except:
- self.currentBranch = 0
- logger.error("Current branch could not be determined, defaulting to \"Stable\"")
- try:
+ logger.info(getcwd())
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
self.localVer = version_file.readline().replace("\n", "")
except:
self.localVer = False
+ try:
+ self.currentBranch = self.get_branch(self.context.settings)
+ except:
+ self.currentBranch = 0
+ logger.error("Current branch could not be determined, defaulting to \"Stable\"")
+
if context:
context.web_app.add_routes([
web.post("/updater/{method_name}", self._handle_server_method_call)
@@ -63,9 +63,21 @@ class Updater:
res["success"] = False
return web.json_response(res)
- async def get_branch(self, manager: SettingsManager):
- logger.debug("current branch: %i" % manager.getSetting("branch", -1))
- return manager.getSetting("branch", -1)
+ def get_branch(self, manager: SettingsManager):
+ ver = manager.getSetting("branch", -1)
+ logger.debug("current branch: %i" % ver)
+ if ver == -1:
+ logger.info("Current branch is not set, determining branch from version...")
+ if self.localVer.startswith("v") and self.localVer.find("-pre"):
+ logger.info("Current version determined to be pre-release")
+ return 1
+ else:
+ logger.info("Current version determined to be stable")
+ return 0
+ return ver
+
+ async def _get_branch(self, manager: SettingsManager):
+ return self.get_branch(manager)
async def get_version(self):
if self.localVer:
@@ -80,7 +92,7 @@ class Updater:
async def check_for_updates(self):
logger.debug("checking for updates")
- selectedBranch = await self.get_branch(self.context.settings)
+ selectedBranch = self.get_branch(self.context.settings)
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()
diff --git a/backend/utilities.py b/backend/utilities.py
index b3431cb6..853f60d2 100644
--- a/backend/utilities.py
+++ b/backend/utilities.py
@@ -1,4 +1,5 @@
import uuid
+import os
from json.decoder import JSONDecodeError
from aiohttp import ClientSession, web
@@ -24,7 +25,8 @@ class Utilities:
"allow_remote_debugging": self.allow_remote_debugging,
"disallow_remote_debugging": self.disallow_remote_debugging,
"set_setting": self.set_setting,
- "get_setting": self.get_setting
+ "get_setting": self.get_setting,
+ "filepicker_ls": self.filepicker_ls
}
if context:
@@ -166,3 +168,29 @@ class Utilities:
async def disallow_remote_debugging(self):
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
return True
+
+ async def filepicker_ls(self, path, include_files=True):
+ # def sorter(file): # Modification time
+ # if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
+ # return os.path.getmtime(os.path.join(path, file))
+ # return 0
+ # file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
+ file_names = sorted(os.listdir(path)) # Alphabetical
+
+ files = []
+
+ for file in file_names:
+ full_path = os.path.join(path, file)
+ is_dir = os.path.isdir(full_path)
+
+ if is_dir or include_files:
+ files.append({
+ "isdir": is_dir,
+ "name": file,
+ "realpath": os.path.realpath(full_path)
+ })
+
+ return {
+ "realpath": os.path.realpath(path),
+ "files": files
+ }