mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-27 21:49:13 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1db083749 | |||
| 0b718daa47 | |||
| 897e1773a5 | |||
| 4e92d4bfc5 | |||
| 1edcc09020 | |||
| e3e1cf2df7 | |||
| 93587fe33b | |||
| 8fab487153 | |||
| b6fce46081 | |||
| e20fd5042c | |||
| 1f596e5a10 | |||
| 6edc3bb658 | |||
| b33d44c53d | |||
| 1379a40a89 | |||
| d48fc885a3 | |||
| 7675775527 | |||
| 1320b13507 | |||
| 52777bc2a4 | |||
| 0929b9c5cb | |||
| 43b2269ea7 | |||
| 0c4e27cd34 | |||
| 36cf85b08a | |||
| 994da868af | |||
| 2e53fb217a |
@@ -0,0 +1,27 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Run linters
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2 # Check out the repository first.
|
||||||
|
- name: Run prettier (JavaScript & TypeScript)
|
||||||
|
run: |
|
||||||
|
pushd frontend
|
||||||
|
npm install
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
- name: Run black (Python formatting)
|
||||||
|
uses: lgeiger/black-action@v1.0.1
|
||||||
|
with:
|
||||||
|
args: "./backend --experimental-string-processing --config ./backend/pyproject.toml"
|
||||||
|
|
||||||
|
- name: Run ruff (Python linting)
|
||||||
|
uses: jpetrucciani/ruff-check@main
|
||||||
|
with:
|
||||||
|
path: "./backend"
|
||||||
@@ -36,6 +36,7 @@ For more information about Decky Loader as well as documentation and development
|
|||||||
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader.
|
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader.
|
||||||
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
|
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
|
||||||
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
|
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
|
||||||
|
- If you run the installer and it just opens a file in a text editor: click the (...) button in the top right of dolphin (the file manager) then 'configure' and 'configure dolphin'. Click on the 'confirmations' tab and set 'when opening an executable file' to 'run script'.
|
||||||
|
|
||||||
## 💾 Installation
|
## 💾 Installation
|
||||||
- This installation can be done without an admin/sudo password set.
|
- This installation can be done without an admin/sudo password set.
|
||||||
|
|||||||
+2
-2
@@ -26,10 +26,10 @@ cd ..
|
|||||||
|
|
||||||
if [[ "$type" == "release" ]]; then
|
if [[ "$type" == "release" ]]; then
|
||||||
printf "release!\n"
|
printf "release!\n"
|
||||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts
|
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||||
elif [[ "$type" == "prerelease" ]]; then
|
elif [[ "$type" == "prerelease" ]]; then
|
||||||
printf "prerelease!\n"
|
printf "prerelease!\n"
|
||||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts
|
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||||
else
|
else
|
||||||
printf "Release type unspecified/badly specified.\n"
|
printf "Release type unspecified/badly specified.\n"
|
||||||
printf "Options: 'release' or 'prerelease'\n"
|
printf "Options: 'release' or 'prerelease'\n"
|
||||||
|
|||||||
+94
-37
@@ -1,25 +1,33 @@
|
|||||||
# Full imports
|
# Full imports
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# import pprint
|
||||||
|
# from pprint import pformat
|
||||||
|
|
||||||
# Partial imports
|
# Partial imports
|
||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession
|
||||||
from asyncio import get_event_loop, sleep
|
from asyncio import sleep
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
from os import R_OK, W_OK, path, listdir, access, mkdir
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
from time import time
|
from time import time
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
# Local modules
|
# Local modules
|
||||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
from helpers import (
|
||||||
|
get_ssl_context,
|
||||||
|
get_user,
|
||||||
|
get_user_group,
|
||||||
|
download_remote_binary_to_path,
|
||||||
|
)
|
||||||
from injector import get_gamepadui_tab
|
from injector import get_gamepadui_tab
|
||||||
|
|
||||||
logger = getLogger("Browser")
|
logger = getLogger("Browser")
|
||||||
|
|
||||||
|
|
||||||
class PluginInstallContext:
|
class PluginInstallContext:
|
||||||
def __init__(self, artifact, name, version, hash) -> None:
|
def __init__(self, artifact, name, version, hash) -> None:
|
||||||
self.artifact = artifact
|
self.artifact = artifact
|
||||||
@@ -27,6 +35,7 @@ class PluginInstallContext:
|
|||||||
self.version = version
|
self.version = version
|
||||||
self.hash = hash
|
self.hash = hash
|
||||||
|
|
||||||
|
|
||||||
class PluginBrowser:
|
class PluginBrowser:
|
||||||
def __init__(self, plugin_path, plugins, loader) -> None:
|
def __init__(self, plugin_path, plugins, loader) -> None:
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
@@ -41,32 +50,40 @@ class PluginBrowser:
|
|||||||
zip_file = ZipFile(zip)
|
zip_file = ZipFile(zip)
|
||||||
zip_file.extractall(self.plugin_path)
|
zip_file.extractall(self.plugin_path)
|
||||||
plugin_dir = self.find_plugin_folder(name)
|
plugin_dir = self.find_plugin_folder(name)
|
||||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
|
code_chown = call(
|
||||||
|
["chown", "-R", get_user() + ":" + get_user_group(), plugin_dir]
|
||||||
|
)
|
||||||
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
||||||
if code_chown != 0 or code_chmod != 0:
|
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})")
|
logger.error(
|
||||||
|
f"chown/chmod exited with a non-zero exit code (chown: {code_chown},"
|
||||||
|
f" chmod: {code_chmod})"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
|
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
|
||||||
rv = False
|
rv = False
|
||||||
try:
|
try:
|
||||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
packageJsonPath = path.join(pluginBasePath, "package.json")
|
||||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
pluginBinPath = path.join(pluginBasePath, "bin")
|
||||||
|
|
||||||
if access(packageJsonPath, R_OK):
|
if access(packageJsonPath, R_OK):
|
||||||
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
||||||
packageJson = json.load(f)
|
packageJson = json.load(f)
|
||||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
if (
|
||||||
|
"remote_binary" in packageJson
|
||||||
|
and len(packageJson["remote_binary"]) > 0
|
||||||
|
):
|
||||||
# create bin directory if needed.
|
# create bin directory if needed.
|
||||||
rc=call(["chmod", "-R", "777", pluginBasePath])
|
call(["chmod", "-R", "777", pluginBasePath])
|
||||||
if access(pluginBasePath, W_OK):
|
if access(pluginBasePath, W_OK):
|
||||||
|
|
||||||
if not path.exists(pluginBinPath):
|
if not path.exists(pluginBinPath):
|
||||||
mkdir(pluginBinPath)
|
mkdir(pluginBinPath)
|
||||||
|
|
||||||
if not access(pluginBinPath, W_OK):
|
if not access(pluginBinPath, W_OK):
|
||||||
rc=call(["chmod", "-R", "777", pluginBinPath])
|
call(["chmod", "-R", "777", pluginBinPath])
|
||||||
|
|
||||||
rv = True
|
rv = True
|
||||||
for remoteBinary in packageJson["remote_binary"]:
|
for remoteBinary in packageJson["remote_binary"]:
|
||||||
@@ -74,16 +91,29 @@ class PluginBrowser:
|
|||||||
binName = remoteBinary["name"]
|
binName = remoteBinary["name"]
|
||||||
binURL = remoteBinary["url"]
|
binURL = remoteBinary["url"]
|
||||||
binHash = remoteBinary["sha256hash"]
|
binHash = remoteBinary["sha256hash"]
|
||||||
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
|
if not await download_remote_binary_to_path(
|
||||||
|
binURL, binHash, path.join(pluginBinPath, binName)
|
||||||
|
):
|
||||||
rv = False
|
rv = False
|
||||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
raise Exception(
|
||||||
|
"Error Downloading Remote Binary"
|
||||||
|
f" {binName}@{binURL} with hash {binHash} to"
|
||||||
|
f" {path.join(pluginBinPath, binName)}"
|
||||||
|
)
|
||||||
|
|
||||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
call(
|
||||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
[
|
||||||
|
"chown",
|
||||||
|
"-R",
|
||||||
|
get_user() + ":" + get_user_group(),
|
||||||
|
self.plugin_path,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
call(["chmod", "-R", "555", pluginBasePath])
|
||||||
else:
|
else:
|
||||||
rv = True
|
rv = True
|
||||||
logger.debug(f"No Remote Binaries to Download")
|
logger.debug("No Remote Binaries to Download")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rv = False
|
rv = False
|
||||||
logger.debug(str(e))
|
logger.debug(str(e))
|
||||||
@@ -93,12 +123,16 @@ class PluginBrowser:
|
|||||||
def find_plugin_folder(self, name):
|
def find_plugin_folder(self, name):
|
||||||
for folder in listdir(self.plugin_path):
|
for folder in listdir(self.plugin_path):
|
||||||
try:
|
try:
|
||||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
|
with open(
|
||||||
|
path.join(self.plugin_path, folder, "plugin.json"),
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
plugin = json.load(f)
|
plugin = json.load(f)
|
||||||
|
|
||||||
if plugin['name'] == name:
|
if plugin["name"] == name:
|
||||||
return str(path.join(self.plugin_path, folder))
|
return str(path.join(self.plugin_path, folder))
|
||||||
except:
|
except Exception:
|
||||||
logger.debug(f"skipping {folder}")
|
logger.debug(f"skipping {folder}")
|
||||||
|
|
||||||
async def uninstall_plugin(self, name):
|
async def uninstall_plugin(self, name):
|
||||||
@@ -108,18 +142,27 @@ class PluginBrowser:
|
|||||||
try:
|
try:
|
||||||
logger.info("uninstalling " + name)
|
logger.info("uninstalling " + name)
|
||||||
logger.info(" at dir " + self.find_plugin_folder(name))
|
logger.info(" at dir " + self.find_plugin_folder(name))
|
||||||
logger.debug("unloading %s" % str(name))
|
logger.debug("calling frontend unload for %s" % str(name))
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||||
|
logger.debug("result of unload from UI: %s", res)
|
||||||
|
# plugins_snapshot = self.plugins.copy()
|
||||||
|
# snapshot_string = pformat(plugins_snapshot)
|
||||||
|
# logger.debug("current plugins: %s", snapshot_string)
|
||||||
if self.plugins[name]:
|
if self.plugins[name]:
|
||||||
|
logger.debug("Plugin %s was found", name)
|
||||||
self.plugins[name].stop()
|
self.plugins[name].stop()
|
||||||
|
logger.debug("Plugin %s was stopped", name)
|
||||||
del self.plugins[name]
|
del self.plugins[name]
|
||||||
|
logger.debug("Plugin %s was removed from the dictionary", name)
|
||||||
logger.debug("removing files %s" % str(name))
|
logger.debug("removing files %s" % str(name))
|
||||||
rmtree(self.find_plugin_folder(name))
|
rmtree(self.find_plugin_folder(name))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
|
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
|
logger.error(
|
||||||
logger.error(f"Error at %s", exc_info=e)
|
f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled"
|
||||||
|
)
|
||||||
|
logger.error("Error at %s", exc_info=e)
|
||||||
if self.loader.watcher:
|
if self.loader.watcher:
|
||||||
self.loader.watcher.disabled = False
|
self.loader.watcher.disabled = False
|
||||||
|
|
||||||
@@ -131,8 +174,11 @@ class PluginBrowser:
|
|||||||
pluginFolderPath = self.find_plugin_folder(name)
|
pluginFolderPath = self.find_plugin_folder(name)
|
||||||
if pluginFolderPath:
|
if pluginFolderPath:
|
||||||
isInstalled = True
|
isInstalled = True
|
||||||
except:
|
except Exception:
|
||||||
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
|
logger.error(
|
||||||
|
f"Failed to determine if {name} is already installed, continuing"
|
||||||
|
" anyway."
|
||||||
|
)
|
||||||
logger.info(f"Installing {name} (Version: {version})")
|
logger.info(f"Installing {name} (Version: {version})")
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
logger.debug(f"Fetching {artifact}")
|
logger.debug(f"Fetching {artifact}")
|
||||||
@@ -146,22 +192,26 @@ class PluginBrowser:
|
|||||||
try:
|
try:
|
||||||
logger.debug("Uninstalling existing plugin...")
|
logger.debug("Uninstalling existing plugin...")
|
||||||
await self.uninstall_plugin(name)
|
await self.uninstall_plugin(name)
|
||||||
except:
|
except Exception:
|
||||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||||
logger.debug("Unzipping...")
|
logger.debug("Unzipping...")
|
||||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||||
if ret:
|
if ret:
|
||||||
plugin_dir = self.find_plugin_folder(name)
|
plugin_dir = self.find_plugin_folder(name)
|
||||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
ret = await self._download_remote_binaries_for_plugin_with_name(
|
||||||
|
plugin_dir
|
||||||
|
)
|
||||||
if ret:
|
if ret:
|
||||||
logger.info(f"Installed {name} (Version: {version})")
|
logger.info(f"Installed {name} (Version: {version})")
|
||||||
if name in self.loader.plugins:
|
if name in self.loader.plugins:
|
||||||
self.loader.plugins[name].stop()
|
self.loader.plugins[name].stop()
|
||||||
self.loader.plugins.pop(name, None)
|
self.loader.plugins.pop(name, None)
|
||||||
await sleep(1)
|
await sleep(1)
|
||||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
|
self.loader.import_plugin(
|
||||||
|
path.join(plugin_dir, "main.py"), plugin_dir
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
logger.fatal("Failed Downloading Remote Binaries")
|
||||||
else:
|
else:
|
||||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||||
if self.loader.watcher:
|
if self.loader.watcher:
|
||||||
@@ -171,14 +221,21 @@ class PluginBrowser:
|
|||||||
|
|
||||||
async def request_plugin_install(self, artifact, name, version, hash):
|
async def request_plugin_install(self, artifact, name, version, hash):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
self.install_requests[request_id] = PluginInstallContext(
|
||||||
|
artifact, name, version, hash
|
||||||
|
)
|
||||||
tab = await get_gamepadui_tab()
|
tab = await get_gamepadui_tab()
|
||||||
await tab.open_websocket()
|
await tab.open_websocket()
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
|
await tab.evaluate_js(
|
||||||
|
f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}',"
|
||||||
|
f" '{request_id}', '{hash}')"
|
||||||
|
)
|
||||||
|
|
||||||
async def confirm_plugin_install(self, request_id):
|
async def confirm_plugin_install(self, request_id):
|
||||||
request = self.install_requests.pop(request_id)
|
request = self.install_requests.pop(request_id)
|
||||||
await self._install(request.artifact, request.name, request.version, request.hash)
|
await self._install(
|
||||||
|
request.artifact, request.name, request.version, request.hash
|
||||||
|
)
|
||||||
|
|
||||||
def cancel_plugin_install(self, request_id):
|
def cancel_plugin_install(self, request_id):
|
||||||
self.install_requests.pop(request_id)
|
self.install_requests.pop(request_id)
|
||||||
|
|||||||
+61
-19
@@ -6,8 +6,6 @@ import subprocess
|
|||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from subprocess import check_output
|
|
||||||
from time import sleep
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
@@ -24,22 +22,38 @@ ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
|||||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||||
frontend_regex = re.compile("^/frontend/.*")
|
frontend_regex = re.compile("^/frontend/.*")
|
||||||
|
|
||||||
|
|
||||||
def get_ssl_context():
|
def get_ssl_context():
|
||||||
return ssl_ctx
|
return ssl_ctx
|
||||||
|
|
||||||
|
|
||||||
def get_csrf_token():
|
def get_csrf_token():
|
||||||
return csrf_token
|
return csrf_token
|
||||||
|
|
||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
async def csrf_middleware(request, handler):
|
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 str(request.rel_url).startswith("/frontend/") 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 await handler(request)
|
||||||
return Response(text='Forbidden', status='403')
|
return Response(text="Forbidden", status="403")
|
||||||
|
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
def set_user():
|
def set_user():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Get the user id hosting the plugin loader
|
# Get the user id hosting the plugin loader
|
||||||
def get_user_id() -> int:
|
def get_user_id() -> int:
|
||||||
proc_path = os.path.realpath(sys.argv[0])
|
proc_path = os.path.realpath(sys.argv[0])
|
||||||
@@ -47,60 +61,73 @@ def get_user_id() -> int:
|
|||||||
for pw in pws:
|
for pw in pws:
|
||||||
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
|
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
|
||||||
return pw.pw_uid
|
return pw.pw_uid
|
||||||
raise PermissionError("The plugin loader does not seem to be hosted by any known user.")
|
raise PermissionError(
|
||||||
|
"The plugin loader does not seem to be hosted by any known user."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Get the user hosting the plugin loader
|
# Get the user hosting the plugin loader
|
||||||
def get_user() -> str:
|
def get_user() -> str:
|
||||||
return pwd.getpwuid(get_user_id()).pw_name
|
return pwd.getpwuid(get_user_id()).pw_name
|
||||||
|
|
||||||
|
|
||||||
# Get the effective user id of the running process
|
# Get the effective user id of the running process
|
||||||
def get_effective_user_id() -> int:
|
def get_effective_user_id() -> int:
|
||||||
return os.geteuid()
|
return os.geteuid()
|
||||||
|
|
||||||
|
|
||||||
# Get the effective user of the running process
|
# Get the effective user of the running process
|
||||||
def get_effective_user() -> str:
|
def get_effective_user() -> str:
|
||||||
return pwd.getpwuid(get_effective_user_id()).pw_name
|
return pwd.getpwuid(get_effective_user_id()).pw_name
|
||||||
|
|
||||||
|
|
||||||
# Get the effective user group id of the running process
|
# Get the effective user group id of the running process
|
||||||
def get_effective_user_group_id() -> int:
|
def get_effective_user_group_id() -> int:
|
||||||
return os.getegid()
|
return os.getegid()
|
||||||
|
|
||||||
|
|
||||||
# Get the effective user group of the running process
|
# Get the effective user group of the running process
|
||||||
def get_effective_user_group() -> str:
|
def get_effective_user_group() -> str:
|
||||||
return grp.getgrgid(get_effective_user_group_id()).gr_name
|
return grp.getgrgid(get_effective_user_group_id()).gr_name
|
||||||
|
|
||||||
|
|
||||||
# Get the user owner of the given file path.
|
# Get the user owner of the given file path.
|
||||||
def get_user_owner(file_path) -> str:
|
def get_user_owner(file_path) -> str:
|
||||||
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
|
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
|
||||||
|
|
||||||
# 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).gr_name
|
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
def set_user_group() -> str:
|
def set_user_group() -> str:
|
||||||
return get_user_group()
|
return get_user_group()
|
||||||
|
|
||||||
|
|
||||||
# Get the group id of the user hosting the plugin loader
|
# Get the group id of the user hosting the plugin loader
|
||||||
def get_user_group_id() -> int:
|
def get_user_group_id() -> int:
|
||||||
return pwd.getpwuid(get_user_id()).pw_gid
|
return pwd.getpwuid(get_user_id()).pw_gid
|
||||||
|
|
||||||
|
|
||||||
# Get the group of the user hosting the plugin loader
|
# Get the group of the user hosting the plugin loader
|
||||||
def get_user_group() -> str:
|
def get_user_group(file_path) -> str:
|
||||||
return grp.getgrgid(get_user_group_id()).gr_name
|
if file_path:
|
||||||
|
return grp.getgrgid(os.stat(file_path).st_gid).gr_name
|
||||||
|
else:
|
||||||
|
return grp.getgrgid(get_user_group_id()).gr_name
|
||||||
|
|
||||||
|
|
||||||
# Get the default home path unless a user is specified
|
# Get the default home path unless a user is specified
|
||||||
def get_home_path(username = None) -> str:
|
def get_home_path(username=None) -> str:
|
||||||
if username == None:
|
if username is None:
|
||||||
username = get_user()
|
username = get_user()
|
||||||
return pwd.getpwnam(username).pw_dir
|
return pwd.getpwnam(username).pw_dir
|
||||||
|
|
||||||
|
|
||||||
# Get the default homebrew path unless a home_path is specified
|
# Get the default homebrew path unless a home_path is specified
|
||||||
def get_homebrew_path(home_path = None) -> str:
|
def get_homebrew_path(home_path=None) -> str:
|
||||||
if home_path == None:
|
if home_path is None:
|
||||||
home_path = get_home_path()
|
home_path = get_home_path()
|
||||||
return os.path.join(home_path, "homebrew")
|
return os.path.join(home_path, "homebrew")
|
||||||
|
|
||||||
|
|
||||||
# Recursively create path and chown as user
|
# Recursively create path and chown as user
|
||||||
def mkdir_as_user(path):
|
def mkdir_as_user(path):
|
||||||
path = os.path.realpath(path)
|
path = os.path.realpath(path)
|
||||||
@@ -113,11 +140,17 @@ def mkdir_as_user(path):
|
|||||||
chown_path = os.path.join(chown_path, p)
|
chown_path = os.path.join(chown_path, p)
|
||||||
os.chown(chown_path, uid, gid)
|
os.chown(chown_path, uid, gid)
|
||||||
|
|
||||||
|
|
||||||
# Fetches the version of loader
|
# Fetches the version of loader
|
||||||
def get_loader_version() -> str:
|
def get_loader_version() -> str:
|
||||||
with open(os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"), "r", encoding="utf-8") as version_file:
|
with open(
|
||||||
|
os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"),
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as version_file:
|
||||||
return version_file.readline().replace("\n", "")
|
return version_file.readline().replace("\n", "")
|
||||||
|
|
||||||
|
|
||||||
# Download Remote Binaries to local Plugin
|
# Download Remote Binaries to local Plugin
|
||||||
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||||
rv = False
|
rv = False
|
||||||
@@ -130,27 +163,36 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
|||||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||||
if binHash == remoteHash:
|
if binHash == remoteHash:
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
with open(path, 'wb') as f:
|
with open(path, "wb") as f:
|
||||||
f.write(data.getbuffer())
|
f.write(data.getbuffer())
|
||||||
rv = True
|
rv = True
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
raise Exception(
|
||||||
|
f"Fatal Error: Hash Mismatch for remote binary {path}@{url}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
rv = False
|
rv = False
|
||||||
except:
|
except Exception:
|
||||||
rv = False
|
rv = False
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||||
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
res = subprocess.run(
|
||||||
|
["systemctl", "is-active", unit_name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
return res.returncode == 0
|
return res.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||||
cmd = ["systemctl", "stop", unit_name]
|
cmd = ["systemctl", "stop", unit_name]
|
||||||
|
|
||||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
|
||||||
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||||
cmd = ["systemctl", "start", unit_name]
|
cmd = ["systemctl", "start", unit_name]
|
||||||
|
|
||||||
|
|||||||
+178
-122
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
from asyncio import sleep
|
from asyncio import sleep
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from traceback import format_exc
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from aiohttp import ClientSession, WSMsgType
|
from aiohttp import ClientSession
|
||||||
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
||||||
from asyncio.exceptions import TimeoutError
|
from asyncio.exceptions import TimeoutError
|
||||||
import uuid
|
import uuid
|
||||||
@@ -39,9 +38,12 @@ class Tab:
|
|||||||
async for message in self.websocket:
|
async for message in self.websocket:
|
||||||
data = message.json()
|
data = message.json()
|
||||||
yield data
|
yield data
|
||||||
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
|
logger.warn(
|
||||||
|
f"The Tab {self.title} socket has been disconnected while listening for"
|
||||||
|
" messages."
|
||||||
|
)
|
||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
|
|
||||||
async def _send_devtools_cmd(self, dc, receive=True):
|
async def _send_devtools_cmd(self, dc, receive=True):
|
||||||
if self.websocket:
|
if self.websocket:
|
||||||
self.cmd_id += 1
|
self.cmd_id += 1
|
||||||
@@ -54,19 +56,24 @@ class Tab:
|
|||||||
return None
|
return None
|
||||||
raise RuntimeError("Websocket not opened")
|
raise RuntimeError("Websocket not opened")
|
||||||
|
|
||||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
async def evaluate_js(
|
||||||
|
self, js, run_async=False, manage_socket=True, get_result=True
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.open_websocket()
|
await self.open_websocket()
|
||||||
|
|
||||||
res = await self._send_devtools_cmd({
|
res = await self._send_devtools_cmd(
|
||||||
"method": "Runtime.evaluate",
|
{
|
||||||
"params": {
|
"method": "Runtime.evaluate",
|
||||||
"expression": js,
|
"params": {
|
||||||
"userGesture": True,
|
"expression": js,
|
||||||
"awaitPromise": run_async
|
"userGesture": True,
|
||||||
}
|
"awaitPromise": run_async,
|
||||||
}, get_result)
|
},
|
||||||
|
},
|
||||||
|
get_result,
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
@@ -74,9 +81,17 @@ class Tab:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
async def has_global_var(self, var_name, manage_socket=True):
|
async def has_global_var(self, var_name, manage_socket=True):
|
||||||
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
res = await self.evaluate_js(
|
||||||
|
f"window['{var_name}'] !== null && window['{var_name}'] !== undefined",
|
||||||
|
False,
|
||||||
|
manage_socket,
|
||||||
|
)
|
||||||
|
|
||||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
if (
|
||||||
|
"result" not in res
|
||||||
|
or "result" not in res["result"]
|
||||||
|
or "value" not in res["result"]["result"]
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return res["result"]["result"]["value"]
|
return res["result"]["result"]["value"]
|
||||||
@@ -86,9 +101,12 @@ class Tab:
|
|||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.open_websocket()
|
await self.open_websocket()
|
||||||
|
|
||||||
res = await self._send_devtools_cmd({
|
res = await self._send_devtools_cmd(
|
||||||
"method": "Page.close",
|
{
|
||||||
}, False)
|
"method": "Page.close",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
@@ -99,32 +117,42 @@ class Tab:
|
|||||||
"""
|
"""
|
||||||
Enables page domain notifications.
|
Enables page domain notifications.
|
||||||
"""
|
"""
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Page.enable",
|
{
|
||||||
}, False)
|
"method": "Page.enable",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
async def disable(self):
|
async def disable(self):
|
||||||
"""
|
"""
|
||||||
Disables page domain notifications.
|
Disables page domain notifications.
|
||||||
"""
|
"""
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Page.disable",
|
{
|
||||||
}, False)
|
"method": "Page.disable",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
async def refresh(self):
|
async def refresh(self, manage_socket=False):
|
||||||
try:
|
try:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.open_websocket()
|
await self.open_websocket()
|
||||||
|
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Page.reload",
|
{
|
||||||
}, False)
|
"method": "Page.reload",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
async def reload_and_evaluate(self, js, manage_socket=True):
|
async def reload_and_evaluate(self, js, manage_socket=True):
|
||||||
"""
|
"""
|
||||||
Reloads the current tab, with JS to run on load via debugger
|
Reloads the current tab, with JS to run on load via debugger
|
||||||
@@ -133,64 +161,70 @@ class Tab:
|
|||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.open_websocket()
|
await self.open_websocket()
|
||||||
|
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd({"method": "Debugger.enable"}, True)
|
||||||
"method": "Debugger.enable"
|
|
||||||
}, True)
|
|
||||||
|
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Runtime.evaluate",
|
{
|
||||||
"params": {
|
"method": "Runtime.evaluate",
|
||||||
"expression": "location.reload();",
|
"params": {
|
||||||
"userGesture": True,
|
"expression": "location.reload();",
|
||||||
"awaitPromise": False
|
"userGesture": True,
|
||||||
}
|
"awaitPromise": False,
|
||||||
}, False)
|
},
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
breakpoint_res = await self._send_devtools_cmd({
|
breakpoint_res = await self._send_devtools_cmd(
|
||||||
"method": "Debugger.setInstrumentationBreakpoint",
|
{
|
||||||
"params": {
|
"method": "Debugger.setInstrumentationBreakpoint",
|
||||||
"instrumentation": "beforeScriptExecution"
|
"params": {"instrumentation": "beforeScriptExecution"},
|
||||||
}
|
},
|
||||||
}, True)
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(breakpoint_res)
|
logger.info(breakpoint_res)
|
||||||
|
|
||||||
# Page finishes loading when breakpoint hits
|
# Page finishes loading when breakpoint hits
|
||||||
|
|
||||||
for x in range(20):
|
for x in range(20):
|
||||||
# this works around 1/5 of the time, so just send it 8 times.
|
# 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
|
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Runtime.evaluate",
|
{
|
||||||
"params": {
|
"method": "Runtime.evaluate",
|
||||||
"expression": js,
|
"params": {
|
||||||
"userGesture": True,
|
"expression": js,
|
||||||
"awaitPromise": False
|
"userGesture": True,
|
||||||
}
|
"awaitPromise": False,
|
||||||
}, False)
|
},
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Debugger.removeBreakpoint",
|
{
|
||||||
"params": {
|
"method": "Debugger.removeBreakpoint",
|
||||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
"params": {
|
||||||
}
|
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||||
}, False)
|
},
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
for x in range(4):
|
for x in range(4):
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd({"method": "Debugger.resume"}, False)
|
||||||
"method": "Debugger.resume"
|
|
||||||
}, False)
|
|
||||||
|
|
||||||
await self._send_devtools_cmd({
|
await self._send_devtools_cmd({"method": "Debugger.disable"}, True)
|
||||||
"method": "Debugger.disable"
|
|
||||||
}, True)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
return
|
return
|
||||||
|
|
||||||
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
|
async def add_script_to_evaluate_on_new_document(
|
||||||
|
self, js, add_dom_wrapper=True, manage_socket=True, get_result=True
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
|
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
|
||||||
|
|
||||||
@@ -225,35 +259,44 @@ class Tab:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
wrappedjs = """
|
wrappedjs = (
|
||||||
function scriptFunc() {
|
"""
|
||||||
|
function scriptFunc() {{
|
||||||
{js}
|
{js}
|
||||||
}
|
}}
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {{
|
||||||
addEventListener('DOMContentLoaded', () => {
|
addEventListener('DOMContentLoaded', () => {{
|
||||||
scriptFunc();
|
scriptFunc();
|
||||||
});
|
}});
|
||||||
} else {
|
}} else {{
|
||||||
scriptFunc();
|
scriptFunc();
|
||||||
}
|
}}
|
||||||
""".format(js=js) if add_dom_wrapper else js
|
""".format(
|
||||||
|
js=js
|
||||||
|
)
|
||||||
|
if add_dom_wrapper
|
||||||
|
else js
|
||||||
|
)
|
||||||
|
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.open_websocket()
|
await self.open_websocket()
|
||||||
|
|
||||||
res = await self._send_devtools_cmd({
|
res = await self._send_devtools_cmd(
|
||||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
{
|
||||||
"params": {
|
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||||
"source": wrappedjs
|
"params": {"source": wrappedjs},
|
||||||
}
|
},
|
||||||
}, get_result)
|
get_result,
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
|
async def remove_script_to_evaluate_on_new_document(
|
||||||
|
self, script_id, manage_socket=True
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
|
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
|
||||||
|
|
||||||
@@ -267,21 +310,28 @@ class Tab:
|
|||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.open_websocket()
|
await self.open_websocket()
|
||||||
|
|
||||||
res = await self._send_devtools_cmd({
|
await self._send_devtools_cmd(
|
||||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
{
|
||||||
"params": {
|
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||||
"identifier": script_id
|
"params": {"identifier": script_id},
|
||||||
}
|
},
|
||||||
}, False)
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
|
|
||||||
async def has_element(self, element_name, manage_socket=True):
|
async def has_element(self, element_name, manage_socket=True):
|
||||||
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
res = await self.evaluate_js(
|
||||||
|
f"document.getElementById('{element_name}') != null", False, manage_socket
|
||||||
|
)
|
||||||
|
|
||||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
if (
|
||||||
|
"result" not in res
|
||||||
|
or "result" not in res["result"]
|
||||||
|
or "value" not in res["result"]["result"]
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return res["result"]["result"]["value"]
|
return res["result"]["result"]["value"]
|
||||||
@@ -298,23 +348,17 @@ class Tab:
|
|||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
style.textContent = `{style}`;
|
style.textContent = `{style}`;
|
||||||
}})()
|
}})()
|
||||||
""", False, manage_socket)
|
""",
|
||||||
|
False,
|
||||||
|
manage_socket,
|
||||||
|
)
|
||||||
|
|
||||||
if "exceptionDetails" in result["result"]:
|
if "exceptionDetails" in result["result"]:
|
||||||
return {
|
return {"success": False, "result": result["result"]}
|
||||||
"success": False,
|
|
||||||
"result": result["result"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "result": css_id}
|
||||||
"success": True,
|
|
||||||
"result": css_id
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "result": e}
|
||||||
"success": False,
|
|
||||||
"result": e
|
|
||||||
}
|
|
||||||
|
|
||||||
async def remove_css(self, css_id, manage_socket=True):
|
async def remove_css(self, css_id, manage_socket=True):
|
||||||
try:
|
try:
|
||||||
@@ -326,25 +370,24 @@ class Tab:
|
|||||||
if (style.nodeName.toLowerCase() == 'style')
|
if (style.nodeName.toLowerCase() == 'style')
|
||||||
style.parentNode.removeChild(style);
|
style.parentNode.removeChild(style);
|
||||||
}})()
|
}})()
|
||||||
""", False, manage_socket)
|
""",
|
||||||
|
False,
|
||||||
|
manage_socket,
|
||||||
|
)
|
||||||
|
|
||||||
if "exceptionDetails" in result["result"]:
|
if "exceptionDetails" in result["result"]:
|
||||||
return {
|
return {"success": False, "result": result}
|
||||||
"success": False,
|
|
||||||
"result": result
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
"success": True
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "result": e}
|
||||||
"success": False,
|
|
||||||
"result": e
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_steam_resource(self, url):
|
async def get_steam_resource(self, url):
|
||||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
res = await self.evaluate_js(
|
||||||
|
f'(async function test() {{ return await (await fetch("{url}")).text()'
|
||||||
|
" })()",
|
||||||
|
True,
|
||||||
|
)
|
||||||
return res["result"]["result"]["value"]
|
return res["result"]["result"]["value"]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -387,32 +430,45 @@ async def get_tab(tab_name) -> Tab:
|
|||||||
raise ValueError(f"Tab {tab_name} not found")
|
raise ValueError(f"Tab {tab_name} not found")
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
||||||
async def get_tab_lambda(test) -> Tab:
|
async def get_tab_lambda(test) -> Tab:
|
||||||
tabs = await get_tabs()
|
tabs = await get_tabs()
|
||||||
tab = next((i for i in tabs if test(i)), None)
|
tab = next((i for i in tabs if test(i)), None)
|
||||||
if not tab:
|
if not tab:
|
||||||
raise ValueError(f"Tab not found by lambda")
|
raise ValueError("Tab not found by lambda")
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
||||||
def tab_is_gamepadui(t: Tab) -> bool:
|
def tab_is_gamepadui(t: Tab) -> bool:
|
||||||
return "https://steamloopback.host/routes/" in t.url and (t.title == "Steam Shared Context presented by Valve™" or t.title == "Steam" or t.title == "SP")
|
return "https://steamloopback.host/routes/" in t.url and (
|
||||||
|
t.title == "Steam Shared Context presented by Valve™"
|
||||||
|
or t.title == "Steam"
|
||||||
|
or t.title == "SP"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_gamepadui_tab() -> Tab:
|
async def get_gamepadui_tab() -> Tab:
|
||||||
tabs = await get_tabs()
|
tabs = await get_tabs()
|
||||||
tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
|
tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
|
||||||
if not tab:
|
if not tab:
|
||||||
raise ValueError(f"GamepadUI Tab not found")
|
raise ValueError("GamepadUI Tab not found")
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
||||||
async def inject_to_tab(tab_name, js, run_async=False):
|
async def inject_to_tab(tab_name, js, run_async=False):
|
||||||
tab = await get_tab(tab_name)
|
tab = await get_tab(tab_name)
|
||||||
|
|
||||||
return await tab.evaluate_js(js, run_async)
|
return await tab.evaluate_js(js, run_async)
|
||||||
|
|
||||||
|
|
||||||
async def close_old_tabs():
|
async def close_old_tabs():
|
||||||
tabs = await get_tabs()
|
tabs = await get_tabs()
|
||||||
for t in tabs:
|
for t in tabs:
|
||||||
if not t.title or (t.title != "Steam Shared Context presented by Valve™" and t.title != "Steam" and t.title != "SP"):
|
if not t.title or (
|
||||||
|
t.title != "Steam Shared Context presented by Valve™"
|
||||||
|
and t.title != "Steam"
|
||||||
|
and t.title != "SP"
|
||||||
|
):
|
||||||
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||||
await t.close()
|
await t.close()
|
||||||
await sleep(0.5)
|
await sleep(0.5)
|
||||||
|
|||||||
+95
-33
@@ -21,7 +21,7 @@ from plugin import PluginWrapper
|
|||||||
|
|
||||||
class FileChangeHandler(RegexMatchingEventHandler):
|
class FileChangeHandler(RegexMatchingEventHandler):
|
||||||
def __init__(self, queue, plugin_path) -> None:
|
def __init__(self, queue, plugin_path) -> None:
|
||||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
|
super().__init__(regexes=[r"^.*?dist\/index\.js$", r"^.*?main\.py$"])
|
||||||
self.logger = getLogger("file-watcher")
|
self.logger = getLogger("file-watcher")
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
@@ -32,7 +32,9 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
|||||||
return
|
return
|
||||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||||
if exists(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))
|
self.queue.put_nowait(
|
||||||
|
(path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True)
|
||||||
|
)
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
src_path = event.src_path
|
src_path = event.src_path
|
||||||
@@ -62,6 +64,7 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
|||||||
self.logger.debug(f"file modified: {src_path}")
|
self.logger.debug(f"file modified: {src_path}")
|
||||||
self.maybe_reload(src_path)
|
self.maybe_reload(src_path)
|
||||||
|
|
||||||
|
|
||||||
class Loader:
|
class Loader:
|
||||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
@@ -81,18 +84,30 @@ class Loader:
|
|||||||
self.loop.create_task(self.handle_reloads())
|
self.loop.create_task(self.handle_reloads())
|
||||||
self.loop.create_task(self.enable_reload_wait())
|
self.loop.create_task(self.enable_reload_wait())
|
||||||
|
|
||||||
server_instance.add_routes([
|
server_instance.add_routes(
|
||||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
[
|
||||||
web.get("/plugins", self.get_plugins),
|
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
web.get("/plugins", self.get_plugins),
|
||||||
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
|
web.get(
|
||||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
"/plugins/{plugin_name}/frontend_bundle",
|
||||||
|
self.handle_frontend_bundle,
|
||||||
# The following is legacy plugin code.
|
),
|
||||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
web.post(
|
||||||
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
|
"/plugins/{plugin_name}/methods/{method_name}",
|
||||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
|
self.handle_plugin_method_call,
|
||||||
])
|
),
|
||||||
|
web.get(
|
||||||
|
"/plugins/{plugin_name}/assets/{path:.*}",
|
||||||
|
self.handle_plugin_frontend_assets,
|
||||||
|
),
|
||||||
|
# The following is legacy plugin code.
|
||||||
|
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||||
|
web.get(
|
||||||
|
"/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route
|
||||||
|
),
|
||||||
|
web.get("/steam_resource/{path:.+}", self.get_steam_resource),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
async def enable_reload_wait(self):
|
async def enable_reload_wait(self):
|
||||||
if self.live_reload:
|
if self.live_reload:
|
||||||
@@ -107,36 +122,63 @@ class Loader:
|
|||||||
|
|
||||||
async def get_plugins(self, request):
|
async def get_plugins(self, request):
|
||||||
plugins = list(self.plugins.values())
|
plugins = list(self.plugins.values())
|
||||||
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
|
return web.json_response(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": str(i) if not i.legacy else "$LEGACY_" + str(i),
|
||||||
|
"version": i.version,
|
||||||
|
}
|
||||||
|
for i in plugins
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def handle_plugin_frontend_assets(self, request):
|
def handle_plugin_frontend_assets(self, request):
|
||||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
|
file = path.join(
|
||||||
|
self.plugin_path,
|
||||||
|
plugin.plugin_directory,
|
||||||
|
"dist/assets",
|
||||||
|
request.match_info["path"],
|
||||||
|
)
|
||||||
|
|
||||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||||
|
|
||||||
def handle_frontend_bundle(self, request):
|
def handle_frontend_bundle(self, request):
|
||||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||||
|
|
||||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
|
with open(
|
||||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"),
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as bundle:
|
||||||
|
return web.Response(
|
||||||
|
text=bundle.read(), content_type="application/javascript"
|
||||||
|
)
|
||||||
|
|
||||||
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
||||||
try:
|
try:
|
||||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||||
if plugin.name in self.plugins:
|
if plugin.name in self.plugins:
|
||||||
if not "debug" in plugin.flags and refresh:
|
if "debug" not in plugin.flags and refresh:
|
||||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
self.logger.info(
|
||||||
return
|
f"Plugin {plugin.name} is already loaded and has requested to"
|
||||||
else:
|
" not be re-loaded"
|
||||||
self.plugins[plugin.name].stop()
|
)
|
||||||
self.plugins.pop(plugin.name, None)
|
return
|
||||||
|
else:
|
||||||
|
self.plugins[plugin.name].stop()
|
||||||
|
self.plugins.pop(plugin.name, None)
|
||||||
if plugin.passive:
|
if plugin.passive:
|
||||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||||
self.plugins[plugin.name] = plugin.start()
|
self.plugins[plugin.name] = plugin.start()
|
||||||
self.logger.info(f"Loaded {plugin.name}")
|
self.logger.info(f"Loaded {plugin.name}")
|
||||||
if not batch:
|
if not batch:
|
||||||
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
|
self.loop.create_task(
|
||||||
|
self.dispatch_plugin(
|
||||||
|
plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name,
|
||||||
|
plugin.version,
|
||||||
|
)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Could not load {file}. {e}")
|
self.logger.error(f"Could not load {file}. {e}")
|
||||||
print_exc()
|
print_exc()
|
||||||
@@ -148,10 +190,20 @@ class Loader:
|
|||||||
def import_plugins(self):
|
def import_plugins(self):
|
||||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||||
|
|
||||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
directories = [
|
||||||
|
i
|
||||||
|
for i in listdir(self.plugin_path)
|
||||||
|
if path.isdir(path.join(self.plugin_path, i))
|
||||||
|
and path.isfile(path.join(self.plugin_path, i, "plugin.json"))
|
||||||
|
]
|
||||||
for directory in directories:
|
for directory in directories:
|
||||||
self.logger.info(f"found plugin: {directory}")
|
self.logger.info(f"found plugin: {directory}")
|
||||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
self.import_plugin(
|
||||||
|
path.join(self.plugin_path, directory, "main.py"),
|
||||||
|
directory,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_reloads(self):
|
async def handle_reloads(self):
|
||||||
while True:
|
while True:
|
||||||
@@ -168,10 +220,10 @@ class Loader:
|
|||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
args = {}
|
args = {}
|
||||||
try:
|
try:
|
||||||
if method_name.startswith("_"):
|
if method_name.startswith("_"):
|
||||||
raise RuntimeError("Tried to call private method")
|
raise RuntimeError("Tried to call private method")
|
||||||
res["result"] = await plugin.execute_method(method_name, args)
|
res["result"] = await plugin.execute_method(method_name, args)
|
||||||
res["success"] = True
|
res["success"] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
res["result"] = str(e)
|
res["result"] = str(e)
|
||||||
res["success"] = False
|
res["success"] = False
|
||||||
@@ -184,9 +236,14 @@ class Loader:
|
|||||||
can introduce it more smoothly and give people the chance to sample the new features even
|
can introduce it more smoothly and give people the chance to sample the new features even
|
||||||
without plugin support. They will be removed once legacy plugins are no longer relevant.
|
without plugin support. They will be removed once legacy plugins are no longer relevant.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def load_plugin_main_view(self, request):
|
async def load_plugin_main_view(self, request):
|
||||||
plugin = self.plugins[request.match_info["name"]]
|
plugin = self.plugins[request.match_info["name"]]
|
||||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
|
with open(
|
||||||
|
path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html),
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as template:
|
||||||
template_data = template.read()
|
template_data = template.read()
|
||||||
ret = f"""
|
ret = f"""
|
||||||
<script src="/legacy/library.js"></script>
|
<script src="/legacy/library.js"></script>
|
||||||
@@ -210,6 +267,11 @@ class Loader:
|
|||||||
async def get_steam_resource(self, request):
|
async def get_steam_resource(self, request):
|
||||||
tab = await get_tab("SP")
|
tab = await get_tab("SP")
|
||||||
try:
|
try:
|
||||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
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:
|
except Exception as e:
|
||||||
return web.Response(text=str(e), status=400)
|
return web.Response(text=str(e), status=400)
|
||||||
|
|||||||
+72
-31
@@ -1,27 +1,35 @@
|
|||||||
# Change PyInstaller files permissions
|
# Change PyInstaller files permissions
|
||||||
import sys
|
import sys
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
if hasattr(sys, '_MEIPASS'):
|
|
||||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
if hasattr(sys, "_MEIPASS"):
|
||||||
|
call(["chmod", "-R", "755", sys._MEIPASS])
|
||||||
# Full imports
|
# Full imports
|
||||||
from asyncio import new_event_loop, set_event_loop, sleep
|
from asyncio import new_event_loop, set_event_loop, sleep
|
||||||
from json import dumps, loads
|
from logging import basicConfig, getLogger
|
||||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
from os import getenv, path
|
||||||
from os import getenv, chmod, path
|
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
import aiohttp_cors
|
import aiohttp_cors
|
||||||
|
|
||||||
# Partial imports
|
# Partial imports
|
||||||
from aiohttp import client_exceptions, WSMsgType
|
from aiohttp import client_exceptions
|
||||||
from aiohttp.web import Application, Response, get, run_app, static
|
from aiohttp.web import Application, Response, get, run_app, static
|
||||||
from aiohttp_jinja2 import setup as jinja_setup
|
from aiohttp_jinja2 import setup as jinja_setup
|
||||||
|
|
||||||
# local modules
|
# local modules
|
||||||
from browser import PluginBrowser
|
from browser import PluginBrowser
|
||||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
from helpers import (
|
||||||
get_home_path, get_homebrew_path, get_user, get_user_group,
|
REMOTE_DEBUGGER_UNIT,
|
||||||
stop_systemd_unit, start_systemd_unit)
|
csrf_middleware,
|
||||||
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
|
get_csrf_token,
|
||||||
|
get_homebrew_path,
|
||||||
|
get_user,
|
||||||
|
get_user_group,
|
||||||
|
stop_systemd_unit,
|
||||||
|
start_systemd_unit,
|
||||||
|
)
|
||||||
|
from injector import get_gamepadui_tab, Tab, close_old_tabs
|
||||||
from loader import Loader
|
from loader import Loader
|
||||||
from settings import SettingsManager
|
from settings import SettingsManager
|
||||||
from updater import Updater
|
from updater import Updater
|
||||||
@@ -42,35 +50,45 @@ CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
basicConfig(
|
basicConfig(
|
||||||
level=CONFIG["log_level"],
|
level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s"
|
||||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = getLogger("Main")
|
logger = getLogger("Main")
|
||||||
|
|
||||||
|
|
||||||
def chown_plugin_dir():
|
def chown_plugin_dir():
|
||||||
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
|
code_chown = call(["chown", "-R", USER + ":" + GROUP, CONFIG["plugin_path"]])
|
||||||
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||||
if code_chown != 0 or code_chmod != 0:
|
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})")
|
logger.error(
|
||||||
|
f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod:"
|
||||||
|
f" {code_chmod})"
|
||||||
|
)
|
||||||
|
|
||||||
if CONFIG["chown_plugin_path"] == True:
|
|
||||||
|
if CONFIG["chown_plugin_path"] is True:
|
||||||
chown_plugin_dir()
|
chown_plugin_dir()
|
||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
def __init__(self, loop) -> None:
|
def __init__(self, loop) -> None:
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.web_app = Application()
|
self.web_app = Application()
|
||||||
self.web_app.middlewares.append(csrf_middleware)
|
self.web_app.middlewares.append(csrf_middleware)
|
||||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
self.cors = aiohttp_cors.setup(
|
||||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
self.web_app,
|
||||||
expose_headers="*",
|
defaults={
|
||||||
allow_headers="*",
|
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||||
allow_credentials=True
|
expose_headers="*", allow_headers="*", allow_credentials=True
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
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_loader)
|
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_loader
|
||||||
|
)
|
||||||
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
||||||
self.utilities = Utilities(self)
|
self.utilities = Utilities(self)
|
||||||
self.updater = Updater(self)
|
self.updater = Updater(self)
|
||||||
@@ -92,8 +110,12 @@ class PluginManager:
|
|||||||
|
|
||||||
for route in list(self.web_app.router.routes()):
|
for route in list(self.web_app.router.routes()):
|
||||||
self.cors.add(route)
|
self.cors.add(route)
|
||||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
self.web_app.add_routes(
|
||||||
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
[static("/static", path.join(path.dirname(__file__), "static"))]
|
||||||
|
)
|
||||||
|
self.web_app.add_routes(
|
||||||
|
[static("/legacy", path.join(path.dirname(__file__), "legacy"))]
|
||||||
|
)
|
||||||
|
|
||||||
def exception_handler(self, loop, context):
|
def exception_handler(self, loop, context):
|
||||||
if context["message"] == "Unclosed connection":
|
if context["message"] == "Unclosed connection":
|
||||||
@@ -117,7 +139,10 @@ class PluginManager:
|
|||||||
while not tab:
|
while not tab:
|
||||||
try:
|
try:
|
||||||
tab = await get_gamepadui_tab()
|
tab = await get_gamepadui_tab()
|
||||||
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
|
except (
|
||||||
|
client_exceptions.ClientConnectorError,
|
||||||
|
client_exceptions.ServerDisconnectedError,
|
||||||
|
):
|
||||||
if not dc:
|
if not dc:
|
||||||
logger.debug("Couldn't connect to debugger, waiting...")
|
logger.debug("Couldn't connect to debugger, waiting...")
|
||||||
dc = True
|
dc = True
|
||||||
@@ -148,7 +173,7 @@ class PluginManager:
|
|||||||
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
||||||
logger.info("CEF has disconnected...")
|
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.
|
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error("Exception while reading page events " + format_exc())
|
logger.error("Exception while reading page events " + format_exc())
|
||||||
await tab.close_websocket()
|
await tab.close_websocket()
|
||||||
pass
|
pass
|
||||||
@@ -164,13 +189,29 @@ class PluginManager:
|
|||||||
if first:
|
if first:
|
||||||
if await tab.has_global_var("deckyHasLoaded", False):
|
if await tab.has_global_var("deckyHasLoaded", False):
|
||||||
await close_old_tabs()
|
await close_old_tabs()
|
||||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 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)
|
await tab.evaluate_js(
|
||||||
except:
|
"try{if (window.deckyHasLoaded){setTimeout(() => location.reload(),"
|
||||||
|
" 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 Exception:
|
||||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
return run_app(
|
||||||
|
self.web_app,
|
||||||
|
host=CONFIG["server_host"],
|
||||||
|
port=CONFIG["server_port"],
|
||||||
|
loop=self.loop,
|
||||||
|
access_log=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
loop = new_event_loop()
|
loop = new_event_loop()
|
||||||
|
|||||||
+81
-25
@@ -1,8 +1,15 @@
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
from asyncio import (
|
||||||
open_unix_connection, set_event_loop, sleep,
|
Lock,
|
||||||
start_unix_server, IncompleteReadError, LimitOverrunError)
|
get_event_loop,
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
new_event_loop,
|
||||||
|
open_unix_connection,
|
||||||
|
set_event_loop,
|
||||||
|
sleep,
|
||||||
|
start_unix_server,
|
||||||
|
IncompleteReadError,
|
||||||
|
LimitOverrunError,
|
||||||
|
)
|
||||||
from importlib.util import module_from_spec, spec_from_file_location
|
from importlib.util import module_from_spec, spec_from_file_location
|
||||||
from json import dumps, load, loads
|
from json import dumps, load, loads
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
@@ -12,11 +19,11 @@ from signal import SIGINT, signal
|
|||||||
from sys import exit
|
from sys import exit
|
||||||
from time import time
|
from time import time
|
||||||
import helpers
|
import helpers
|
||||||
from updater import Updater
|
|
||||||
|
|
||||||
multiprocessing.set_start_method("fork")
|
multiprocessing.set_start_method("fork")
|
||||||
|
|
||||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
BUFFER_LIMIT = 2**20 # 1 MiB
|
||||||
|
|
||||||
|
|
||||||
class PluginWrapper:
|
class PluginWrapper:
|
||||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||||
@@ -30,12 +37,23 @@ class PluginWrapper:
|
|||||||
|
|
||||||
self.version = None
|
self.version = None
|
||||||
|
|
||||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
json = load(
|
||||||
|
open(
|
||||||
|
path.join(plugin_path, plugin_directory, "plugin.json"),
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
)
|
||||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
package_json = load(
|
||||||
|
open(
|
||||||
|
path.join(plugin_path, plugin_directory, "package.json"),
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
)
|
||||||
self.version = package_json["version"]
|
self.version = package_json["version"]
|
||||||
|
|
||||||
|
|
||||||
self.legacy = False
|
self.legacy = False
|
||||||
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
||||||
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
||||||
@@ -62,18 +80,28 @@ class PluginWrapper:
|
|||||||
setgid(0 if "root" in self.flags else helpers.get_user_group_id())
|
setgid(0 if "root" in self.flags else helpers.get_user_group_id())
|
||||||
setuid(0 if "root" in self.flags else helpers.get_user_id())
|
setuid(0 if "root" in self.flags else helpers.get_user_id())
|
||||||
# export a bunch of environment variables to help plugin developers
|
# export a bunch of environment variables to help plugin developers
|
||||||
environ["HOME"] = helpers.get_home_path("root" if "root" in self.flags else helpers.get_user())
|
environ["HOME"] = helpers.get_home_path(
|
||||||
|
"root" if "root" in self.flags else helpers.get_user()
|
||||||
|
)
|
||||||
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
|
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
|
||||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||||
environ["DECKY_USER"] = helpers.get_user()
|
environ["DECKY_USER"] = helpers.get_user()
|
||||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(
|
||||||
|
environ["DECKY_HOME"], "settings", self.plugin_directory
|
||||||
|
)
|
||||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
||||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(
|
||||||
|
environ["DECKY_HOME"], "data", self.plugin_directory
|
||||||
|
)
|
||||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
||||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(
|
||||||
|
environ["DECKY_HOME"], "logs", self.plugin_directory
|
||||||
|
)
|
||||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
||||||
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
environ["DECKY_PLUGIN_DIR"] = path.join(
|
||||||
|
self.plugin_path, self.plugin_directory
|
||||||
|
)
|
||||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||||
environ["DECKY_PLUGIN_VERSION"] = self.version
|
environ["DECKY_PLUGIN_VERSION"] = self.version
|
||||||
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
||||||
@@ -86,21 +114,32 @@ class PluginWrapper:
|
|||||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||||
get_event_loop().create_task(self._setup_socket())
|
get_event_loop().create_task(self._setup_socket())
|
||||||
get_event_loop().run_forever()
|
get_event_loop().run_forever()
|
||||||
except:
|
except Exception:
|
||||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
async def _unload(self):
|
async def _unload(self):
|
||||||
try:
|
try:
|
||||||
self.log.info("Attempting to unload " + self.name + "\n")
|
self.log.info(
|
||||||
|
"Attempting to unload with plugin "
|
||||||
|
+ self.name
|
||||||
|
+ '\'s "_unload" function.\n'
|
||||||
|
)
|
||||||
if hasattr(self.Plugin, "_unload"):
|
if hasattr(self.Plugin, "_unload"):
|
||||||
await self.Plugin._unload(self.Plugin)
|
await self.Plugin._unload(self.Plugin)
|
||||||
except:
|
self.log.info("Unloaded " + self.name + "\n")
|
||||||
|
else:
|
||||||
|
self.log.info(
|
||||||
|
'Could not find "_unload" in ' + self.name + "'s main.py" + "\n"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
|
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
async def _setup_socket(self):
|
async def _setup_socket(self):
|
||||||
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
self.socket = await start_unix_server(
|
||||||
|
self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT
|
||||||
|
)
|
||||||
|
|
||||||
async def _listen_for_method_call(self, reader, writer):
|
async def _listen_for_method_call(self, reader, writer):
|
||||||
while True:
|
while True:
|
||||||
@@ -118,6 +157,7 @@ class PluginWrapper:
|
|||||||
break
|
break
|
||||||
data = loads(line.decode("utf-8"))
|
data = loads(line.decode("utf-8"))
|
||||||
if "stop" in data:
|
if "stop" in data:
|
||||||
|
self.log.info("Calling Loader unload function.")
|
||||||
await self._unload()
|
await self._unload()
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
while get_event_loop().is_running():
|
while get_event_loop().is_running():
|
||||||
@@ -126,12 +166,14 @@ class PluginWrapper:
|
|||||||
return
|
return
|
||||||
d = {"res": None, "success": True}
|
d = {"res": None, "success": True}
|
||||||
try:
|
try:
|
||||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
d["res"] = await getattr(self.Plugin, data["method"])(
|
||||||
|
self.Plugin, **data["args"]
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d["res"] = str(e)
|
d["res"] = str(e)
|
||||||
d["success"] = False
|
d["success"] = False
|
||||||
finally:
|
finally:
|
||||||
writer.write((dumps(d, ensure_ascii=False)+"\n").encode("utf-8"))
|
writer.write((dumps(d, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
async def _open_socket_if_not_exists(self):
|
async def _open_socket_if_not_exists(self):
|
||||||
@@ -139,9 +181,11 @@ class PluginWrapper:
|
|||||||
retries = 0
|
retries = 0
|
||||||
while retries < 10:
|
while retries < 10:
|
||||||
try:
|
try:
|
||||||
self.reader, self.writer = await open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
self.reader, self.writer = await open_unix_connection(
|
||||||
|
self.socket_addr, limit=BUFFER_LIMIT
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
except:
|
except Exception:
|
||||||
await sleep(2)
|
await sleep(2)
|
||||||
retries += 1
|
retries += 1
|
||||||
return False
|
return False
|
||||||
@@ -157,20 +201,32 @@ class PluginWrapper:
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
if self.passive:
|
if self.passive:
|
||||||
return
|
return
|
||||||
|
|
||||||
async def _(self):
|
async def _(self):
|
||||||
if await self._open_socket_if_not_exists():
|
if await self._open_socket_if_not_exists():
|
||||||
self.writer.write((dumps({ "stop": True }, ensure_ascii=False)+"\n").encode("utf-8"))
|
self.writer.write(
|
||||||
|
(dumps({"stop": True}, ensure_ascii=False) + "\n").encode("utf-8")
|
||||||
|
)
|
||||||
await self.writer.drain()
|
await self.writer.drain()
|
||||||
self.writer.close()
|
self.writer.close()
|
||||||
|
|
||||||
get_event_loop().create_task(_(self))
|
get_event_loop().create_task(_(self))
|
||||||
|
|
||||||
async def execute_method(self, method_name, kwargs):
|
async def execute_method(self, method_name, kwargs):
|
||||||
if self.passive:
|
if self.passive:
|
||||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
raise RuntimeError(
|
||||||
|
"This plugin is passive (aka does not implement main.py)"
|
||||||
|
)
|
||||||
async with self.method_call_lock:
|
async with self.method_call_lock:
|
||||||
if await self._open_socket_if_not_exists():
|
if await self._open_socket_if_not_exists():
|
||||||
self.writer.write(
|
self.writer.write(
|
||||||
(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False) + "\n").encode("utf-8"))
|
(
|
||||||
|
dumps(
|
||||||
|
{"method": method_name, "args": kwargs}, ensure_ascii=False
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
).encode("utf-8")
|
||||||
|
)
|
||||||
await self.writer.drain()
|
await self.writer.drain()
|
||||||
line = bytearray()
|
line = bytearray()
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 88
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
ignore = [
|
||||||
|
# Ignore line length check and let Black handle it
|
||||||
|
"E501",
|
||||||
|
|
||||||
|
# Ignore SyntaxError due to ruff not supporting pattern matching
|
||||||
|
# https://github.com/charliermarsh/ruff/issues/282
|
||||||
|
"E999",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Assume Python 3.10.
|
||||||
|
target-version = "py310"
|
||||||
+13
-10
@@ -2,33 +2,36 @@ from json import dump, load
|
|||||||
from os import mkdir, path, listdir, rename
|
from os import mkdir, path, listdir, rename
|
||||||
from shutil import chown
|
from shutil import chown
|
||||||
|
|
||||||
from helpers import get_home_path, get_homebrew_path, get_user, get_user_group, get_user_owner
|
from helpers import (
|
||||||
|
get_homebrew_path,
|
||||||
|
get_user,
|
||||||
|
get_user_group,
|
||||||
|
get_user_owner,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
def __init__(self, name, settings_directory = None) -> None:
|
def __init__(self, name, settings_directory=None) -> None:
|
||||||
USER = get_user()
|
USER = get_user()
|
||||||
GROUP = get_user_group()
|
GROUP = get_user_group()
|
||||||
wrong_dir = get_homebrew_path()
|
wrong_dir = get_homebrew_path()
|
||||||
if settings_directory == None:
|
if settings_directory is None:
|
||||||
settings_directory = path.join(wrong_dir, "settings")
|
settings_directory = path.join(wrong_dir, "settings")
|
||||||
|
|
||||||
self.path = path.join(settings_directory, name + ".json")
|
self.path = path.join(settings_directory, name + ".json")
|
||||||
|
|
||||||
#Create the folder with the correct permission
|
# Create the folder with the correct permission
|
||||||
if not path.exists(settings_directory):
|
if not path.exists(settings_directory):
|
||||||
mkdir(settings_directory)
|
mkdir(settings_directory)
|
||||||
chown(settings_directory, USER, GROUP)
|
chown(settings_directory, USER, GROUP)
|
||||||
|
|
||||||
#Copy all old settings file in the root directory to the correct folder
|
# Copy all old settings file in the root directory to the correct folder
|
||||||
for file in listdir(wrong_dir):
|
for file in listdir(wrong_dir):
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
rename(path.join(wrong_dir,file),
|
rename(path.join(wrong_dir, file), path.join(settings_directory, file))
|
||||||
path.join(settings_directory, file))
|
|
||||||
self.path = path.join(settings_directory, name + ".json")
|
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 the owner of the settings directory is not the user, then set it as the user:
|
|
||||||
if get_user_owner(settings_directory) != USER:
|
if get_user_owner(settings_directory) != USER:
|
||||||
chown(settings_directory, USER, GROUP)
|
chown(settings_directory, USER, GROUP)
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ class SettingsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
open(self.path, "x", encoding="utf-8")
|
open(self.path, "x", encoding="utf-8")
|
||||||
except FileExistsError as e:
|
except FileExistsError:
|
||||||
self.read()
|
self.read()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
+81
-26
@@ -16,6 +16,7 @@ from settings import SettingsManager
|
|||||||
|
|
||||||
logger = getLogger("Updater")
|
logger = getLogger("Updater")
|
||||||
|
|
||||||
|
|
||||||
class Updater:
|
class Updater:
|
||||||
def __init__(self, context) -> None:
|
def __init__(self, context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -26,7 +27,7 @@ class Updater:
|
|||||||
"get_version": self.get_version,
|
"get_version": self.get_version,
|
||||||
"do_update": self.do_update,
|
"do_update": self.do_update,
|
||||||
"do_restart": self.do_restart,
|
"do_restart": self.do_restart,
|
||||||
"check_for_updates": self.check_for_updates
|
"check_for_updates": self.check_for_updates,
|
||||||
}
|
}
|
||||||
self.remoteVer = None
|
self.remoteVer = None
|
||||||
self.allRemoteVers = None
|
self.allRemoteVers = None
|
||||||
@@ -39,12 +40,14 @@ class Updater:
|
|||||||
self.currentBranch = self.get_branch(self.context.settings)
|
self.currentBranch = self.get_branch(self.context.settings)
|
||||||
except:
|
except:
|
||||||
self.currentBranch = 0
|
self.currentBranch = 0
|
||||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
logger.error(
|
||||||
|
'Current branch could not be determined, defaulting to "Stable"'
|
||||||
|
)
|
||||||
|
|
||||||
if context:
|
if context:
|
||||||
context.web_app.add_routes([
|
context.web_app.add_routes(
|
||||||
web.post("/updater/{method_name}", self._handle_server_method_call)
|
[web.post("/updater/{method_name}", self._handle_server_method_call)]
|
||||||
])
|
)
|
||||||
context.loop.create_task(self.version_reloader())
|
context.loop.create_task(self.version_reloader())
|
||||||
|
|
||||||
async def _handle_server_method_call(self, request):
|
async def _handle_server_method_call(self, request):
|
||||||
@@ -89,7 +92,10 @@ class Updater:
|
|||||||
case 1 | 2:
|
case 1 | 2:
|
||||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||||
case _:
|
case _:
|
||||||
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
|
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/main/dist/plugin_loader-prerelease.service"
|
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||||
return str(url)
|
return str(url)
|
||||||
|
|
||||||
@@ -99,31 +105,58 @@ class Updater:
|
|||||||
"current": self.localVer,
|
"current": self.localVer,
|
||||||
"remote": self.remoteVer,
|
"remote": self.remoteVer,
|
||||||
"all": self.allRemoteVers,
|
"all": self.allRemoteVers,
|
||||||
"updatable": self.localVer != None
|
"updatable": self.localVer != None,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {"current": "unknown", "remote": self.remoteVer, "all": self.allRemoteVers, "updatable": False}
|
return {
|
||||||
|
"current": "unknown",
|
||||||
|
"remote": self.remoteVer,
|
||||||
|
"all": self.allRemoteVers,
|
||||||
|
"updatable": False,
|
||||||
|
}
|
||||||
|
|
||||||
async def check_for_updates(self):
|
async def check_for_updates(self):
|
||||||
logger.debug("checking for updates")
|
logger.debug("checking for updates")
|
||||||
selectedBranch = self.get_branch(self.context.settings)
|
selectedBranch = self.get_branch(self.context.settings)
|
||||||
async with ClientSession() as web:
|
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:
|
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()
|
remoteVersions = await res.json()
|
||||||
self.allRemoteVers = remoteVersions
|
self.allRemoteVers = remoteVersions
|
||||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||||
if selectedBranch == 0:
|
if selectedBranch == 0:
|
||||||
logger.debug("release type: release")
|
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)
|
self.remoteVer = next(
|
||||||
|
filter(
|
||||||
|
lambda ver: ver["tag_name"].startswith("v")
|
||||||
|
and not ver["prerelease"]
|
||||||
|
and ver["tag_name"],
|
||||||
|
remoteVersions,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
elif selectedBranch == 1:
|
elif selectedBranch == 1:
|
||||||
logger.debug("release type: pre-release")
|
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)
|
self.remoteVer = next(
|
||||||
|
filter(
|
||||||
|
lambda ver: ver["prerelease"]
|
||||||
|
and ver["tag_name"].startswith("v")
|
||||||
|
and ver["tag_name"].find("-pre"),
|
||||||
|
remoteVersions,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.error("release type: NOT FOUND")
|
logger.error("release type: NOT FOUND")
|
||||||
raise ValueError("no valid branch found")
|
raise ValueError("no valid branch found")
|
||||||
logger.info("Updated remote version information")
|
logger.info("Updated remote version information")
|
||||||
tab = await get_gamepadui_tab()
|
tab = await get_gamepadui_tab()
|
||||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
await tab.evaluate_js(
|
||||||
|
f"window.DeckyPluginLoader.notifyUpdates()", False, True, False
|
||||||
|
)
|
||||||
return await self.get_version()
|
return await self.get_version()
|
||||||
|
|
||||||
async def version_reloader(self):
|
async def version_reloader(self):
|
||||||
@@ -133,7 +166,7 @@ class Updater:
|
|||||||
await self.check_for_updates()
|
await self.check_for_updates()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
await sleep(60 * 60 * 6) # 6 hours
|
await sleep(60 * 60 * 6) # 6 hours
|
||||||
|
|
||||||
async def do_update(self):
|
async def do_update(self):
|
||||||
logger.debug("Starting update.")
|
logger.debug("Starting update.")
|
||||||
@@ -147,7 +180,9 @@ class Updater:
|
|||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
logger.debug("Downloading systemd service")
|
logger.debug("Downloading systemd service")
|
||||||
# download the relevant systemd service depending upon branch
|
# download the relevant systemd service depending upon branch
|
||||||
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
async with web.request(
|
||||||
|
"GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True
|
||||||
|
) as res:
|
||||||
logger.debug("Downloading service file")
|
logger.debug("Downloading service file")
|
||||||
data = await res.content.read()
|
data = await res.content.read()
|
||||||
logger.debug(str(data))
|
logger.debug(str(data))
|
||||||
@@ -157,22 +192,33 @@ class Updater:
|
|||||||
out.write(data)
|
out.write(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error at %s", exc_info=e)
|
logger.error(f"Error at %s", exc_info=e)
|
||||||
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
|
with open(
|
||||||
|
path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8"
|
||||||
|
) as service_file:
|
||||||
service_data = service_file.read()
|
service_data = service_file.read()
|
||||||
service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
|
service_data = service_data.replace(
|
||||||
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
|
"${HOMEBREW_FOLDER}", helpers.get_homebrew_path()
|
||||||
service_file.write(service_data)
|
)
|
||||||
|
with open(
|
||||||
|
path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8"
|
||||||
|
) as service_file:
|
||||||
|
service_file.write(service_data)
|
||||||
|
|
||||||
logger.debug("Saved service file")
|
logger.debug("Saved service file")
|
||||||
logger.debug("Copying service file over current file.")
|
logger.debug("Copying service file over current file.")
|
||||||
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
|
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
|
||||||
if not os.path.exists(path.join(getcwd(), ".systemd")):
|
if not os.path.exists(path.join(getcwd(), ".systemd")):
|
||||||
os.mkdir(path.join(getcwd(), ".systemd"))
|
os.mkdir(path.join(getcwd(), ".systemd"))
|
||||||
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
shutil.move(
|
||||||
|
service_file_path,
|
||||||
|
path.join(getcwd(), ".systemd") + "/plugin_loader.service",
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Downloading binary")
|
logger.debug("Downloading binary")
|
||||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
async with web.request(
|
||||||
total = int(res.headers.get('content-length', 0))
|
"GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True
|
||||||
|
) as res:
|
||||||
|
total = int(res.headers.get("content-length", 0))
|
||||||
# we need to not delete the binary until we have downloaded the new binary!
|
# we need to not delete the binary until we have downloaded the new binary!
|
||||||
try:
|
try:
|
||||||
remove(path.join(getcwd(), "PluginLoader"))
|
remove(path.join(getcwd(), "PluginLoader"))
|
||||||
@@ -186,13 +232,22 @@ class Updater:
|
|||||||
raw += len(c)
|
raw += len(c)
|
||||||
new_progress = round((raw / total) * 100)
|
new_progress = round((raw / total) * 100)
|
||||||
if progress != new_progress:
|
if progress != new_progress:
|
||||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
self.context.loop.create_task(
|
||||||
|
tab.evaluate_js(
|
||||||
|
f"window.DeckyUpdater.updateProgress({new_progress})",
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
)
|
||||||
progress = new_progress
|
progress = new_progress
|
||||||
|
|
||||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
with open(
|
||||||
|
path.join(getcwd(), ".loader.version"), "w", encoding="utf-8"
|
||||||
|
) as out:
|
||||||
out.write(version)
|
out.write(version)
|
||||||
|
|
||||||
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
|
call(["chmod", "+x", path.join(getcwd(), "PluginLoader")])
|
||||||
logger.info("Updated loader installation.")
|
logger.info("Updated loader installation.")
|
||||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||||
await self.do_restart()
|
await self.do_restart()
|
||||||
|
|||||||
+54
-73
@@ -3,13 +3,12 @@ import os
|
|||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from asyncio import sleep, start_server, gather, open_connection
|
from asyncio import start_server, gather, open_connection
|
||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession, web
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
|
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
|
||||||
import helpers
|
import helpers
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class Utilities:
|
class Utilities:
|
||||||
@@ -31,7 +30,7 @@ class Utilities:
|
|||||||
"get_setting": self.get_setting,
|
"get_setting": self.get_setting,
|
||||||
"filepicker_ls": self.filepicker_ls,
|
"filepicker_ls": self.filepicker_ls,
|
||||||
"disable_rdt": self.disable_rdt,
|
"disable_rdt": self.disable_rdt,
|
||||||
"enable_rdt": self.enable_rdt
|
"enable_rdt": self.enable_rdt,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger = getLogger("Utilities")
|
self.logger = getLogger("Utilities")
|
||||||
@@ -41,9 +40,9 @@ class Utilities:
|
|||||||
self.rdt_proxy_task = None
|
self.rdt_proxy_task = None
|
||||||
|
|
||||||
if context:
|
if context:
|
||||||
context.web_app.add_routes([
|
context.web_app.add_routes(
|
||||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
[web.post("/methods/{method_name}", self._handle_server_method_call)]
|
||||||
])
|
)
|
||||||
|
|
||||||
async def _handle_server_method_call(self, request):
|
async def _handle_server_method_call(self, request):
|
||||||
method_name = request.match_info["method_name"]
|
method_name = request.match_info["method_name"]
|
||||||
@@ -61,12 +60,11 @@ class Utilities:
|
|||||||
res["success"] = False
|
res["success"] = False
|
||||||
return web.json_response(res)
|
return web.json_response(res)
|
||||||
|
|
||||||
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False):
|
async def install_plugin(
|
||||||
|
self, artifact="", name="No name", version="dev", hash=False
|
||||||
|
):
|
||||||
return await self.context.plugin_browser.request_plugin_install(
|
return await self.context.plugin_browser.request_plugin_install(
|
||||||
artifact=artifact,
|
artifact=artifact, name=name, version=version, hash=hash
|
||||||
name=name,
|
|
||||||
version=version,
|
|
||||||
hash=hash
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def confirm_plugin_install(self, request_id):
|
async def confirm_plugin_install(self, request_id):
|
||||||
@@ -80,13 +78,11 @@ class Utilities:
|
|||||||
|
|
||||||
async def http_request(self, method="", url="", **kwargs):
|
async def http_request(self, method="", url="", **kwargs):
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
|
res = await web.request(
|
||||||
|
method, url, ssl=helpers.get_ssl_context(), **kwargs
|
||||||
|
)
|
||||||
text = await res.text()
|
text = await res.text()
|
||||||
return {
|
return {"status": res.status, "headers": dict(res.headers), "body": text}
|
||||||
"status": res.status,
|
|
||||||
"headers": dict(res.headers),
|
|
||||||
"body": text
|
|
||||||
}
|
|
||||||
|
|
||||||
async def ping(self, **kwargs):
|
async def ping(self, **kwargs):
|
||||||
return "pong"
|
return "pong"
|
||||||
@@ -95,26 +91,18 @@ class Utilities:
|
|||||||
try:
|
try:
|
||||||
result = await inject_to_tab(tab, code, run_async)
|
result = await inject_to_tab(tab, code, run_async)
|
||||||
if "exceptionDetails" in result["result"]:
|
if "exceptionDetails" in result["result"]:
|
||||||
return {
|
return {"success": False, "result": result["result"]}
|
||||||
"success": False,
|
|
||||||
"result": result["result"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "result": result["result"]["result"].get("value")}
|
||||||
"success": True,
|
|
||||||
"result": result["result"]["result"].get("value")
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "result": e}
|
||||||
"success": False,
|
|
||||||
"result": e
|
|
||||||
}
|
|
||||||
|
|
||||||
async def inject_css_into_tab(self, tab, style):
|
async def inject_css_into_tab(self, tab, style):
|
||||||
try:
|
try:
|
||||||
css_id = str(uuid.uuid4())
|
css_id = str(uuid.uuid4())
|
||||||
|
|
||||||
result = await inject_to_tab(tab,
|
result = await inject_to_tab(
|
||||||
|
tab,
|
||||||
f"""
|
f"""
|
||||||
(function() {{
|
(function() {{
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
@@ -122,27 +110,21 @@ class Utilities:
|
|||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
style.textContent = `{style}`;
|
style.textContent = `{style}`;
|
||||||
}})()
|
}})()
|
||||||
""", False)
|
""",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
if "exceptionDetails" in result["result"]:
|
if "exceptionDetails" in result["result"]:
|
||||||
return {
|
return {"success": False, "result": result["result"]}
|
||||||
"success": False,
|
|
||||||
"result": result["result"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "result": css_id}
|
||||||
"success": True,
|
|
||||||
"result": css_id
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "result": e}
|
||||||
"success": False,
|
|
||||||
"result": e
|
|
||||||
}
|
|
||||||
|
|
||||||
async def remove_css_from_tab(self, tab, css_id):
|
async def remove_css_from_tab(self, tab, css_id):
|
||||||
try:
|
try:
|
||||||
result = await inject_to_tab(tab,
|
result = await inject_to_tab(
|
||||||
|
tab,
|
||||||
f"""
|
f"""
|
||||||
(function() {{
|
(function() {{
|
||||||
let style = document.getElementById("{css_id}");
|
let style = document.getElementById("{css_id}");
|
||||||
@@ -150,22 +132,16 @@ class Utilities:
|
|||||||
if (style.nodeName.toLowerCase() == 'style')
|
if (style.nodeName.toLowerCase() == 'style')
|
||||||
style.parentNode.removeChild(style);
|
style.parentNode.removeChild(style);
|
||||||
}})()
|
}})()
|
||||||
""", False)
|
""",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
if "exceptionDetails" in result["result"]:
|
if "exceptionDetails" in result["result"]:
|
||||||
return {
|
return {"success": False, "result": result}
|
||||||
"success": False,
|
|
||||||
"result": result
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
"success": True
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "result": e}
|
||||||
"success": False,
|
|
||||||
"result": e
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_setting(self, key, default):
|
async def get_setting(self, key, default):
|
||||||
return self.context.settings.getSetting(key, default)
|
return self.context.settings.getSetting(key, default)
|
||||||
@@ -187,7 +163,7 @@ class Utilities:
|
|||||||
# return os.path.getmtime(os.path.join(path, file))
|
# return os.path.getmtime(os.path.join(path, file))
|
||||||
# return 0
|
# return 0
|
||||||
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
|
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
|
||||||
file_names = sorted(os.listdir(path)) # Alphabetical
|
file_names = sorted(os.listdir(path)) # Alphabetical
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
|
|
||||||
@@ -196,16 +172,15 @@ class Utilities:
|
|||||||
is_dir = os.path.isdir(full_path)
|
is_dir = os.path.isdir(full_path)
|
||||||
|
|
||||||
if is_dir or include_files:
|
if is_dir or include_files:
|
||||||
files.append({
|
files.append(
|
||||||
"isdir": is_dir,
|
{
|
||||||
"name": file,
|
"isdir": is_dir,
|
||||||
"realpath": os.path.realpath(full_path)
|
"name": file,
|
||||||
})
|
"realpath": os.path.realpath(full_path),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {"realpath": os.path.realpath(path), "files": files}
|
||||||
"realpath": os.path.realpath(path),
|
|
||||||
"files": files
|
|
||||||
}
|
|
||||||
|
|
||||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||||
def start_rdt_proxy(self, ip, port):
|
def start_rdt_proxy(self, ip, port):
|
||||||
@@ -215,10 +190,10 @@ class Utilities:
|
|||||||
writer.write(await reader.read(2048))
|
writer.write(await reader.read(2048))
|
||||||
finally:
|
finally:
|
||||||
writer.close()
|
writer.close()
|
||||||
|
|
||||||
async def handle_client(local_reader, local_writer):
|
async def handle_client(local_reader, local_writer):
|
||||||
try:
|
try:
|
||||||
remote_reader, remote_writer = await open_connection(
|
remote_reader, remote_writer = await open_connection(ip, port)
|
||||||
ip, port)
|
|
||||||
pipe1 = pipe(local_reader, remote_writer)
|
pipe1 = pipe(local_reader, remote_writer)
|
||||||
pipe2 = pipe(remote_reader, local_writer)
|
pipe2 = pipe(remote_reader, local_writer)
|
||||||
await gather(pipe1, pipe2)
|
await gather(pipe1, pipe2)
|
||||||
@@ -239,11 +214,14 @@ class Utilities:
|
|||||||
self.stop_rdt_proxy()
|
self.stop_rdt_proxy()
|
||||||
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
||||||
|
|
||||||
if ip != None:
|
if ip is not None:
|
||||||
self.logger.info("Connecting to React DevTools at " + ip)
|
self.logger.info("Connecting to React DevTools at " + ip)
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
|
res = await web.request(
|
||||||
script = """
|
"GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()
|
||||||
|
)
|
||||||
|
script = (
|
||||||
|
"""
|
||||||
if (!window.deckyHasConnectedRDT) {
|
if (!window.deckyHasConnectedRDT) {
|
||||||
window.deckyHasConnectedRDT = true;
|
window.deckyHasConnectedRDT = true;
|
||||||
// This fixes the overlay when hovering over an element in RDT
|
// This fixes the overlay when hovering over an element in RDT
|
||||||
@@ -254,7 +232,10 @@ class Utilities:
|
|||||||
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
|
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
""" + await res.text() + "\n}"
|
"""
|
||||||
|
+ await res.text()
|
||||||
|
+ "\n}"
|
||||||
|
)
|
||||||
if res.status != 200:
|
if res.status != 200:
|
||||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||||
return False
|
return False
|
||||||
@@ -265,7 +246,7 @@ class Utilities:
|
|||||||
await close_old_tabs()
|
await close_old_tabs()
|
||||||
result = await tab.reload_and_evaluate(script)
|
result = await tab.reload_and_evaluate(script)
|
||||||
self.logger.info(result)
|
self.logger.info(result)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.error("Failed to connect to React DevTools")
|
self.logger.error("Failed to connect to React DevTools")
|
||||||
self.logger.error(format_exc())
|
self.logger.error(format_exc())
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export default function DeckyIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456">
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: 'none' }}
|
||||||
|
d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
|
||||||
|
c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
|
||||||
|
c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
|
||||||
|
c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
|
||||||
|
C226.38,87.12,191.11,72.51,154.33,72.51z"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ellipse
|
||||||
|
transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
|
||||||
|
style={{ fill: 'none' }}
|
||||||
|
cx="154.33"
|
||||||
|
cy="211.33"
|
||||||
|
rx="69.33"
|
||||||
|
ry="69.33"
|
||||||
|
/>
|
||||||
|
<path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
|
||||||
|
<path
|
||||||
|
style={{ fill: 'currentColor' }}
|
||||||
|
d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
|
||||||
|
C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
|
||||||
|
c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
|
||||||
|
c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
|
||||||
|
c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
|
||||||
|
c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
|
||||||
|
c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
|
||||||
|
c7.18,0,13,5.82,13,13V271z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||||
import { CSSProperties, VFC } from 'react';
|
import { CSSProperties, VFC } from 'react';
|
||||||
import { FaArrowLeft, FaCog, FaStore } from 'react-icons/fa';
|
import { BsGearFill } from 'react-icons/bs';
|
||||||
|
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||||
|
|
||||||
import { useDeckyState } from './DeckyState';
|
import { useDeckyState } from './DeckyState';
|
||||||
|
|
||||||
@@ -26,12 +27,6 @@ const TitleView: VFC = () => {
|
|||||||
if (activePlugin === null) {
|
if (activePlugin === null) {
|
||||||
return (
|
return (
|
||||||
<Focusable style={titleStyles} className={staticClasses.Title}>
|
<Focusable style={titleStyles} className={staticClasses.Title}>
|
||||||
<DialogButton
|
|
||||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
|
||||||
onClick={onSettingsClick}
|
|
||||||
>
|
|
||||||
<FaCog style={{ marginTop: '-4px', display: 'block' }} />
|
|
||||||
</DialogButton>
|
|
||||||
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
|
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
|
||||||
<DialogButton
|
<DialogButton
|
||||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||||
@@ -39,6 +34,12 @@ const TitleView: VFC = () => {
|
|||||||
>
|
>
|
||||||
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
|
<DialogButton
|
||||||
|
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||||
|
onClick={onSettingsClick}
|
||||||
|
>
|
||||||
|
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
|
||||||
|
</DialogButton>
|
||||||
</Focusable>
|
</Focusable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
import { FaCode, FaPlug } from 'react-icons/fa';
|
||||||
|
|
||||||
import { useSetting } from '../../utils/hooks/useSetting';
|
import { useSetting } from '../../utils/hooks/useSetting';
|
||||||
|
import DeckyIcon from '../DeckyIcon';
|
||||||
import WithSuspense from '../WithSuspense';
|
import WithSuspense from '../WithSuspense';
|
||||||
import GeneralSettings from './pages/general';
|
import GeneralSettings from './pages/general';
|
||||||
import PluginList from './pages/plugin_list';
|
import PluginList from './pages/plugin_list';
|
||||||
@@ -13,19 +15,18 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
const pages = [
|
const pages = [
|
||||||
{
|
{
|
||||||
title: 'General',
|
title: 'Decky',
|
||||||
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
||||||
route: '/decky/settings/general',
|
route: '/decky/settings/general',
|
||||||
|
icon: <DeckyIcon />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Plugins',
|
title: 'Plugins',
|
||||||
content: <PluginList />,
|
content: <PluginList />,
|
||||||
route: '/decky/settings/plugins',
|
route: '/decky/settings/plugins',
|
||||||
|
icon: <FaPlug />,
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (isDeveloper)
|
|
||||||
pages.push({
|
|
||||||
title: 'Developer',
|
title: 'Developer',
|
||||||
content: (
|
content: (
|
||||||
<WithSuspense>
|
<WithSuspense>
|
||||||
@@ -33,7 +34,10 @@ export default function SettingsPage() {
|
|||||||
</WithSuspense>
|
</WithSuspense>
|
||||||
),
|
),
|
||||||
route: '/decky/settings/developer',
|
route: '/decky/settings/developer',
|
||||||
});
|
icon: <FaCode />,
|
||||||
|
visible: isDeveloper,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return <SidebarNavigation title="Decky Settings" showTitle pages={pages} />;
|
return <SidebarNavigation pages={pages} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Field, Focusable, TextField, Toggle } from 'decky-frontend-lib';
|
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||||
|
|
||||||
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||||
|
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
||||||
|
|
||||||
export default function DeveloperSettings() {
|
export default function DeveloperSettings() {
|
||||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||||
@@ -12,7 +13,8 @@ export default function DeveloperSettings() {
|
|||||||
const textRef = useRef<HTMLDivElement>(null);
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DialogBody>
|
||||||
|
<RemoteDebuggingSettings />
|
||||||
<Field
|
<Field
|
||||||
label="Enable Valve Internal"
|
label="Enable Valve Internal"
|
||||||
description={
|
description={
|
||||||
@@ -30,55 +32,33 @@ export default function DeveloperSettings() {
|
|||||||
setShowValveInternal(toggleValue);
|
setShowValveInternal(toggleValue);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Field>{' '}
|
</Field>
|
||||||
<Focusable
|
<Field
|
||||||
onTouchEnd={
|
label="Enable React DevTools"
|
||||||
reactDevtoolsIP == ''
|
description={
|
||||||
? () => {
|
<>
|
||||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
<span style={{ whiteSpace: 'pre-line' }}>
|
||||||
}
|
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
|
||||||
: undefined
|
IP address before enabling.
|
||||||
}
|
</span>
|
||||||
onClick={
|
<br />
|
||||||
reactDevtoolsIP == ''
|
<br />
|
||||||
? () => {
|
<div ref={textRef}>
|
||||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||||
}
|
</div>
|
||||||
: undefined
|
</>
|
||||||
}
|
|
||||||
onOKButton={
|
|
||||||
reactDevtoolsIP == ''
|
|
||||||
? () => {
|
|
||||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
|
icon={<FaReact style={{ display: 'block' }} />}
|
||||||
>
|
>
|
||||||
<Field
|
<Toggle
|
||||||
label="Enable React DevTools"
|
value={reactDevtoolsEnabled}
|
||||||
description={
|
disabled={reactDevtoolsIP == ''}
|
||||||
<>
|
onChange={(toggleValue) => {
|
||||||
<span style={{ whiteSpace: 'pre-line' }}>
|
setReactDevtoolsEnabled(toggleValue);
|
||||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
|
setShouldConnectToReactDevTools(toggleValue);
|
||||||
the IP address before enabling.
|
}}
|
||||||
</span>
|
/>
|
||||||
<div ref={textRef}>
|
</Field>
|
||||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
</DialogBody>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
icon={<FaReact style={{ display: 'block' }} />}
|
|
||||||
>
|
|
||||||
<Toggle
|
|
||||||
value={reactDevtoolsEnabled}
|
|
||||||
disabled={reactDevtoolsIP == ''}
|
|
||||||
onChange={(toggleValue) => {
|
|
||||||
setReactDevtoolsEnabled(toggleValue);
|
|
||||||
setShouldConnectToReactDevTools(toggleValue);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Focusable>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
|||||||
return (
|
return (
|
||||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||||
// 0 being stable, 1 being pre-release and 2 being nightly
|
// 0 being stable, 1 being pre-release and 2 being nightly
|
||||||
<Field label="Update Channel">
|
<Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
rgOptions={Object.values(UpdateBranch)
|
rgOptions={Object.values(UpdateBranch)
|
||||||
.filter((branch) => typeof branch == 'string')
|
.filter((branch) => typeof branch == 'string')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Field, Toggle } from 'decky-frontend-lib';
|
import { Field, Toggle } from 'decky-frontend-lib';
|
||||||
import { FaBug } from 'react-icons/fa';
|
import { FaChrome } from 'react-icons/fa';
|
||||||
|
|
||||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||||
|
|
||||||
@@ -11,10 +11,10 @@ export default function RemoteDebuggingSettings() {
|
|||||||
label="Allow Remote CEF Debugging"
|
label="Allow Remote CEF Debugging"
|
||||||
description={
|
description={
|
||||||
<span style={{ whiteSpace: 'pre-line' }}>
|
<span style={{ whiteSpace: 'pre-line' }}>
|
||||||
Allow unauthenticated access to the CEF debugger to anyone in your network
|
Allows unauthenticated access to the CEF debugger to anyone in your network.
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
icon={<FaBug style={{ display: 'block' }} />}
|
icon={<FaChrome style={{ display: 'block' }} />}
|
||||||
>
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
value={allowRemoteDebugging || false}
|
value={allowRemoteDebugging || false}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const StoreSelect: FunctionComponent<{}> = () => {
|
|||||||
// 0 being Default, 1 being Testing and 2 being Custom
|
// 0 being Default, 1 being Testing and 2 being Custom
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label="Store Channel">
|
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
rgOptions={Object.values(Store)
|
rgOptions={Object.values(Store)
|
||||||
.filter((store) => typeof store == 'string')
|
.filter((store) => typeof store == 'string')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Suspense, lazy } from 'react';
|
import { Suspense, lazy } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FaArrowDown } from 'react-icons/fa';
|
import { FaExclamation } from 'react-icons/fa';
|
||||||
|
|
||||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||||
import { findSP } from '../../../../utils/windows';
|
import { findSP } from '../../../../utils/windows';
|
||||||
@@ -95,21 +95,21 @@ export default function UpdaterSettings() {
|
|||||||
<Field
|
<Field
|
||||||
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
|
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
|
||||||
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
|
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
|
||||||
label="Updates"
|
label="Decky Updates"
|
||||||
description={
|
description={
|
||||||
versionInfo && (
|
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
|
||||||
<span style={{ whiteSpace: 'pre-line' }}>{`Current version: ${versionInfo.current}\n${
|
''
|
||||||
versionInfo.updatable ? `Latest version: ${versionInfo.remote?.tag_name}` : ''
|
) : (
|
||||||
}`}</span>
|
<span>Up to date: running {versionInfo?.current}</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
icon={
|
icon={
|
||||||
!versionInfo ? (
|
versionInfo?.remote &&
|
||||||
<Spinner style={{ width: '1em', height: 20, display: 'block' }} />
|
versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||||
) : (
|
<FaExclamation color="var(--gpColor-Yellow)" style={{ display: 'block' }} />
|
||||||
<FaArrowDown style={{ display: 'block' }} />
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
childrenContainerWidth={'fixed'}
|
||||||
>
|
>
|
||||||
{updateProgress == -1 && !isLoaderUpdating ? (
|
{updateProgress == -1 && !isLoaderUpdating ? (
|
||||||
<DialogButton
|
<DialogButton
|
||||||
@@ -144,7 +144,7 @@ export default function UpdaterSettings() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
{versionInfo?.remote && (
|
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||||
<InlinePatchNotes
|
<InlinePatchNotes
|
||||||
title={versionInfo?.remote.name}
|
title={versionInfo?.remote.name}
|
||||||
date={new Intl.RelativeTimeFormat('en-US', {
|
date={new Intl.RelativeTimeFormat('en-US', {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { DialogButton, Field, TextField, Toggle } from 'decky-frontend-lib';
|
import {
|
||||||
|
DialogBody,
|
||||||
|
DialogButton,
|
||||||
|
DialogControlsSection,
|
||||||
|
DialogControlsSectionHeader,
|
||||||
|
Field,
|
||||||
|
TextField,
|
||||||
|
Toggle,
|
||||||
|
} from 'decky-frontend-lib';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaShapes, FaTools } from 'react-icons/fa';
|
|
||||||
|
|
||||||
import { installFromURL } from '../../../../store';
|
import { installFromURL } from '../../../../store';
|
||||||
|
import { useDeckyState } from '../../../DeckyState';
|
||||||
import BranchSelect from './BranchSelect';
|
import BranchSelect from './BranchSelect';
|
||||||
import RemoteDebuggingSettings from './RemoteDebugging';
|
|
||||||
import StoreSelect from './StoreSelect';
|
import StoreSelect from './StoreSelect';
|
||||||
import UpdaterSettings from './Updater';
|
import UpdaterSettings from './Updater';
|
||||||
|
|
||||||
@@ -16,34 +23,44 @@ export default function GeneralSettings({
|
|||||||
setIsDeveloper: (val: boolean) => void;
|
setIsDeveloper: (val: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [pluginURL, setPluginURL] = useState('');
|
const [pluginURL, setPluginURL] = useState('');
|
||||||
|
const { versionInfo } = useDeckyState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<DialogBody>
|
||||||
<UpdaterSettings />
|
<DialogControlsSection>
|
||||||
<BranchSelect />
|
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
|
||||||
<StoreSelect />
|
<UpdaterSettings />
|
||||||
<RemoteDebuggingSettings />
|
</DialogControlsSection>
|
||||||
<Field
|
<DialogControlsSection>
|
||||||
label="Developer mode"
|
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
|
||||||
description={<span style={{ whiteSpace: 'pre-line' }}>Enables Decky's developer settings.</span>}
|
<BranchSelect />
|
||||||
icon={<FaTools style={{ display: 'block' }} />}
|
<StoreSelect />
|
||||||
>
|
</DialogControlsSection>
|
||||||
<Toggle
|
<DialogControlsSection>
|
||||||
value={isDeveloper}
|
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||||
onChange={(toggleValue) => {
|
<Field label="Enable Developer Mode">
|
||||||
setIsDeveloper(toggleValue);
|
<Toggle
|
||||||
}}
|
value={isDeveloper}
|
||||||
/>
|
onChange={(toggleValue) => {
|
||||||
</Field>
|
setIsDeveloper(toggleValue);
|
||||||
<Field
|
}}
|
||||||
label="Manual plugin install"
|
/>
|
||||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
</Field>
|
||||||
icon={<FaShapes style={{ display: 'block' }} />}
|
<Field
|
||||||
>
|
label="Install plugin from URL"
|
||||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||||
Install
|
>
|
||||||
</DialogButton>
|
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||||
</Field>
|
Install
|
||||||
</div>
|
</DialogButton>
|
||||||
|
</Field>
|
||||||
|
</DialogControlsSection>
|
||||||
|
<DialogControlsSection>
|
||||||
|
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
||||||
|
<Field label="Decky Version" focusable={true}>
|
||||||
|
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
|
||||||
|
</Field>
|
||||||
|
</DialogControlsSection>
|
||||||
|
</DialogBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
|
import {
|
||||||
|
DialogBody,
|
||||||
|
DialogButton,
|
||||||
|
DialogControlsSection,
|
||||||
|
Focusable,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
showContextMenu,
|
||||||
|
} from 'decky-frontend-lib';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
||||||
|
|
||||||
@@ -21,46 +29,52 @@ export default function PluginList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul style={{ listStyleType: 'none' }}>
|
<DialogBody>
|
||||||
{plugins.map(({ name, version }) => {
|
<DialogControlsSection>
|
||||||
const update = updates?.get(name);
|
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||||
return (
|
{plugins.map(({ name, version }) => {
|
||||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
const update = updates?.get(name);
|
||||||
<span>
|
return (
|
||||||
{name} {version}
|
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||||
</span>
|
<span>
|
||||||
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
{name} <span style={{ opacity: '50%' }}>{'(' + version + ')'}</span>
|
||||||
{update && (
|
</span>
|
||||||
<DialogButton
|
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
{update && (
|
||||||
onClick={() => requestPluginInstall(name, update)}
|
<DialogButton
|
||||||
>
|
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
onClick={() => requestPluginInstall(name, update)}
|
||||||
Update to {update.name}
|
>
|
||||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
</div>
|
Update to {update.name}
|
||||||
</DialogButton>
|
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||||
)}
|
</div>
|
||||||
<DialogButton
|
</DialogButton>
|
||||||
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
)}
|
||||||
onClick={(e: MouseEvent) =>
|
<DialogButton
|
||||||
showContextMenu(
|
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
||||||
<Menu label="Plugin Actions">
|
onClick={(e: MouseEvent) =>
|
||||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
showContextMenu(
|
||||||
Reload
|
<Menu label="Plugin Actions">
|
||||||
</MenuItem>
|
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
||||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
Reload
|
||||||
</Menu>,
|
</MenuItem>
|
||||||
e.currentTarget ?? window,
|
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>
|
||||||
)
|
Uninstall
|
||||||
}
|
</MenuItem>
|
||||||
>
|
</Menu>,
|
||||||
<FaEllipsisH />
|
e.currentTarget ?? window,
|
||||||
</DialogButton>
|
)
|
||||||
</Focusable>
|
}
|
||||||
</li>
|
>
|
||||||
);
|
<FaEllipsisH />
|
||||||
})}
|
</DialogButton>
|
||||||
</ul>
|
</Focusable>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</DialogControlsSection>
|
||||||
|
</DialogBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ class PluginLoader extends Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public unloadPlugin(name: string) {
|
public unloadPlugin(name: string) {
|
||||||
|
console.log('Plugin List: ', this.plugins);
|
||||||
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
||||||
plugin?.onDismount?.();
|
plugin?.onDismount?.();
|
||||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||||
@@ -335,7 +336,7 @@ class PluginLoader extends Logger {
|
|||||||
fetchNoCors(url: string, request: any = {}) {
|
fetchNoCors(url: string, request: any = {}) {
|
||||||
let args = { method: 'POST', headers: {} };
|
let args = { method: 'POST', headers: {} };
|
||||||
const req = { ...args, ...request, url, data: request.body };
|
const req = { ...args, ...request, url, data: request.body };
|
||||||
req?.body && delete req.body
|
req?.body && delete req.body;
|
||||||
return this.callServerMethod('http_request', req);
|
return this.callServerMethod('http_request', req);
|
||||||
},
|
},
|
||||||
executeInTab(tab: string, runAsync: boolean, code: string) {
|
executeInTab(tab: string, runAsync: boolean, code: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user