mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-12 01:51:54 +00:00
Add cross-platform support to decky (#387)
* Import generic watchdog observer over platform specific import * Use os.path rather than genericpath * Split off socket management in plugin.py * Don't specify multiprocessing start type Default on linux is already fork * Move all platform-specific functions to seperate files TODO: make plugin.py platform agnostic * fix import * add backwards compat to helpers.py * add backwards compatibility to helpers.py harder * Testing autobuild for win * Testing autobuild for win, try 2 * Testing autobuild for win, try 3 * Testing autobuild for win, try 4 * Create the plugins folder before attempting to use it * Implement win get_username() * Create win install script * Fix branch guess from version * Create .loader.version in install script * Add .cmd shim to facilitate auto-restarts * Properly fix branch guess from version * Fix updater on windows * Try 2 of fixing updates for windows * Test * pain * Update install script * Powershell doesn't believe in utf8 * Powershell good * add ON_LINUX variable to localplatform * Fix more merge issues * test * Move custom imports to main.py * Move custom imports to after __main__ check Due to windows' default behaviour being spawn, it will spawn a new process and thus import into sys.path multiple times * Log errors in get_system_pythonpaths() and get_loader_version() + split get_system_pythonpaths() on newline * Remove whitespace in result of get_system_pythonpaths() * use python3 on linux and python on windows in get_system_pythonpaths() * Remove fork-specific urls * Fix MIME types not working on Windows
This commit is contained in:
committed by
GitHub
parent
faf46ba533
commit
fd325ef1cc
@@ -84,6 +84,49 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ./dist/PluginLoader
|
path: ./dist/PluginLoader
|
||||||
|
|
||||||
|
build-win:
|
||||||
|
name: Build PluginLoader for Win
|
||||||
|
runs-on: windows-2022
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🧰
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up NodeJS 18 💎
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Set up Python 3.10.2 🐍
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10.2"
|
||||||
|
|
||||||
|
- name: Install Python dependencies ⬇️
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pyinstaller==5.5
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Install JS dependencies ⬇️
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: |
|
||||||
|
npm i -g pnpm
|
||||||
|
pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build JS Frontend 🛠️
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Build Python Backend 🛠️
|
||||||
|
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/legacy;/legacy" --add-data "./plugin;/plugin" ./backend/main.py
|
||||||
|
|
||||||
|
- name: Upload package artifact ⬆️
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: PluginLoader Win
|
||||||
|
path: ./dist/PluginLoader.exe
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release stable version of the package
|
name: Release stable version of the package
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
|
||||||
|
|||||||
+9
-10
@@ -12,12 +12,12 @@ 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, rename, listdir, access, mkdir
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from subprocess import call
|
|
||||||
from time import time
|
from time import time
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
from localplatform import chown, chmod
|
||||||
|
|
||||||
# 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, download_remote_binary_to_path
|
||||||
from injector import get_gamepadui_tab
|
from injector import get_gamepadui_tab
|
||||||
|
|
||||||
logger = getLogger("Browser")
|
logger = getLogger("Browser")
|
||||||
@@ -43,10 +43,9 @@ class PluginBrowser:
|
|||||||
zip_file = ZipFile(zip)
|
zip_file = ZipFile(zip)
|
||||||
zip_file.extractall(self.plugin_path)
|
zip_file.extractall(self.plugin_path)
|
||||||
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
||||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
|
|
||||||
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
|
||||||
if code_chown != 0 or code_chmod != 0:
|
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -61,14 +60,14 @@ class PluginBrowser:
|
|||||||
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])
|
chmod(pluginBasePath, 777)
|
||||||
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])
|
chmod(pluginBinPath, 777)
|
||||||
|
|
||||||
rv = True
|
rv = True
|
||||||
for remoteBinary in packageJson["remote_binary"]:
|
for remoteBinary in packageJson["remote_binary"]:
|
||||||
@@ -80,8 +79,8 @@ class PluginBrowser:
|
|||||||
rv = False
|
rv = False
|
||||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||||
|
|
||||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
chown(self.plugin_path)
|
||||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
chmod(pluginBasePath, 555)
|
||||||
else:
|
else:
|
||||||
rv = True
|
rv = True
|
||||||
logger.debug(f"No Remote Binaries to Download")
|
logger.debug(f"No Remote Binaries to Download")
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class UserType(Enum):
|
||||||
|
HOST_USER = 1
|
||||||
|
EFFECTIVE_USER = 2
|
||||||
|
ROOT = 3
|
||||||
+83
-88
@@ -1,19 +1,18 @@
|
|||||||
import grp
|
|
||||||
import pwd
|
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from subprocess import check_output
|
import subprocess
|
||||||
from time import sleep
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import certifi
|
import certifi
|
||||||
from aiohttp.web import Response, middleware
|
from aiohttp.web import Response, middleware
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
import localplatform
|
||||||
|
from customtypes import UserType
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||||
|
|
||||||
@@ -23,6 +22,7 @@ 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/.*")
|
||||||
|
logger = getLogger("Main")
|
||||||
|
|
||||||
def get_ssl_context():
|
def get_ssl_context():
|
||||||
return ssl_ctx
|
return ssl_ctx
|
||||||
@@ -36,97 +36,41 @@ async def csrf_middleware(request, handler):
|
|||||||
return await handler(request)
|
return await handler(request)
|
||||||
return Response(text='Forbidden', status='403')
|
return Response(text='Forbidden', status='403')
|
||||||
|
|
||||||
# Deprecated
|
|
||||||
def set_user():
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Get the user id hosting the plugin loader
|
|
||||||
def get_user_id() -> int:
|
|
||||||
proc_path = os.path.realpath(sys.argv[0])
|
|
||||||
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
|
||||||
for pw in pws:
|
|
||||||
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
|
|
||||||
return pw.pw_uid
|
|
||||||
raise PermissionError("The plugin loader does not seem to be hosted by any known user.")
|
|
||||||
|
|
||||||
# Get the user hosting the plugin loader
|
|
||||||
def get_user() -> str:
|
|
||||||
return pwd.getpwuid(get_user_id()).pw_name
|
|
||||||
|
|
||||||
# Get the effective user id of the running process
|
|
||||||
def get_effective_user_id() -> int:
|
|
||||||
return os.geteuid()
|
|
||||||
|
|
||||||
# Get the effective user of the running process
|
|
||||||
def get_effective_user() -> str:
|
|
||||||
return pwd.getpwuid(get_effective_user_id()).pw_name
|
|
||||||
|
|
||||||
# Get the effective user group id of the running process
|
|
||||||
def get_effective_user_group_id() -> int:
|
|
||||||
return os.getegid()
|
|
||||||
|
|
||||||
# Get the effective user group of the running process
|
|
||||||
def get_effective_user_group() -> str:
|
|
||||||
return grp.getgrgid(get_effective_user_group_id()).gr_name
|
|
||||||
|
|
||||||
# Get the user owner of the given file path.
|
|
||||||
def get_user_owner(file_path) -> str:
|
|
||||||
return pwd.getpwuid(os.stat(file_path).st_uid).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
|
|
||||||
def set_user_group() -> str:
|
|
||||||
return get_user_group()
|
|
||||||
|
|
||||||
# Get the group id of the user hosting the plugin loader
|
|
||||||
def get_user_group_id() -> int:
|
|
||||||
return pwd.getpwuid(get_user_id()).pw_gid
|
|
||||||
|
|
||||||
# Get the group of the user hosting the plugin loader
|
|
||||||
def get_user_group() -> str:
|
|
||||||
return grp.getgrgid(get_user_group_id()).gr_name
|
|
||||||
|
|
||||||
# Get the default home path unless a user is specified
|
|
||||||
def get_home_path(username = None) -> str:
|
|
||||||
if username == None:
|
|
||||||
username = get_user()
|
|
||||||
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:
|
return os.path.join(home_path if home_path != None else localplatform.get_home_path(), "homebrew")
|
||||||
home_path = get_home_path()
|
|
||||||
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)
|
||||||
os.makedirs(path, exist_ok=True)
|
os.makedirs(path, exist_ok=True)
|
||||||
chown_path = get_home_path()
|
localplatform.chown(path)
|
||||||
parts = os.path.relpath(path, chown_path).split(os.sep)
|
|
||||||
uid = get_user_id()
|
|
||||||
gid = get_user_group_id()
|
|
||||||
for p in parts:
|
|
||||||
chown_path = os.path.join(chown_path, p)
|
|
||||||
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:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||||
return version_file.readline().strip()
|
return version_file.readline().strip()
|
||||||
except:
|
except Exception as e:
|
||||||
|
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
# returns the appropriate system python paths
|
# returns the appropriate system python paths
|
||||||
def get_system_pythonpaths() -> list[str]:
|
def get_system_pythonpaths() -> list[str]:
|
||||||
|
extra_args = {}
|
||||||
|
|
||||||
|
if localplatform.ON_LINUX:
|
||||||
# run as normal normal user to also include user python paths
|
# run as normal normal user to also include user python paths
|
||||||
proc = subprocess.run(["python3", "-c", "import sys; print(':'.join(x for x in sys.path if x))"],
|
extra_args["user"] = localplatform.localplatform._get_user_id()
|
||||||
user=get_user_id(), env={}, capture_output=True)
|
extra_args["env"] = {}
|
||||||
return proc.stdout.decode().strip().split(":")
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
|
||||||
|
capture_output=True, **extra_args)
|
||||||
|
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -152,16 +96,67 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
|||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
# Deprecated
|
||||||
|
def set_user():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Deprecated
|
||||||
|
def set_user_group() -> str:
|
||||||
|
return get_user_group()
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Below is legacy code, provided for backwards compatibility. This will break on windows
|
||||||
|
#########
|
||||||
|
|
||||||
|
# Get the user id hosting the plugin loader
|
||||||
|
def get_user_id() -> int:
|
||||||
|
return localplatform.localplatform._get_user_id()
|
||||||
|
|
||||||
|
# Get the user hosting the plugin loader
|
||||||
|
def get_user() -> str:
|
||||||
|
return localplatform.localplatform._get_user()
|
||||||
|
|
||||||
|
# Get the effective user id of the running process
|
||||||
|
def get_effective_user_id() -> int:
|
||||||
|
return localplatform.localplatform._get_effective_user_id()
|
||||||
|
|
||||||
|
# Get the effective user of the running process
|
||||||
|
def get_effective_user() -> str:
|
||||||
|
return localplatform.localplatform._get_effective_user()
|
||||||
|
|
||||||
|
# Get the effective user group id of the running process
|
||||||
|
def get_effective_user_group_id() -> int:
|
||||||
|
return localplatform.localplatform._get_effective_user_group_id()
|
||||||
|
|
||||||
|
# Get the effective user group of the running process
|
||||||
|
def get_effective_user_group() -> str:
|
||||||
|
return localplatform.localplatform._get_effective_user_group()
|
||||||
|
|
||||||
|
# Get the user owner of the given file path.
|
||||||
|
def get_user_owner(file_path) -> str:
|
||||||
|
return localplatform.localplatform._get_user_owner(file_path)
|
||||||
|
|
||||||
|
# Get the user group of the given file path.
|
||||||
|
def get_user_group(file_path) -> str:
|
||||||
|
return localplatform.localplatform._get_user_group(file_path)
|
||||||
|
|
||||||
|
# Get the group id of the user hosting the plugin loader
|
||||||
|
def get_user_group_id() -> int:
|
||||||
|
return localplatform.localplatform._get_user_group_id()
|
||||||
|
|
||||||
|
# Get the group of the user hosting the plugin loader
|
||||||
|
def get_user_group() -> str:
|
||||||
|
return localplatform.localplatform._get_user_group()
|
||||||
|
|
||||||
|
# Get the default home path unless a user is specified
|
||||||
|
def get_home_path(username = None) -> str:
|
||||||
|
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
|
||||||
|
|
||||||
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)
|
return await localplatform.service_active(unit_name)
|
||||||
return res.returncode == 0
|
|
||||||
|
|
||||||
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
async def stop_systemd_unit(unit_name: str) -> bool:
|
||||||
cmd = ["systemctl", "stop", unit_name]
|
return await localplatform.service_stop(unit_name)
|
||||||
|
|
||||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
async def start_systemd_unit(unit_name: str) -> bool:
|
||||||
|
return await localplatform.service_start(unit_name)
|
||||||
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
|
||||||
cmd = ["systemctl", "start", unit_name]
|
|
||||||
|
|
||||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
||||||
@@ -13,7 +13,6 @@ from watchdog.observers import Observer
|
|||||||
from injector import get_tab, get_gamepadui_tab
|
from injector import get_tab, get_gamepadui_tab
|
||||||
from plugin import PluginWrapper
|
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$'])
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import platform
|
||||||
|
|
||||||
|
ON_WINDOWS = platform.system() == "Windows"
|
||||||
|
ON_LINUX = not ON_WINDOWS
|
||||||
|
|
||||||
|
if ON_WINDOWS:
|
||||||
|
from localplatformwin import *
|
||||||
|
import localplatformwin as localplatform
|
||||||
|
else:
|
||||||
|
from localplatformlinux import *
|
||||||
|
import localplatformlinux as localplatform
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import os, pwd, grp, sys
|
||||||
|
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||||
|
from customtypes import UserType
|
||||||
|
|
||||||
|
# Get the user id hosting the plugin loader
|
||||||
|
def _get_user_id() -> int:
|
||||||
|
proc_path = os.path.realpath(sys.argv[0])
|
||||||
|
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
||||||
|
for pw in pws:
|
||||||
|
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
|
||||||
|
return pw.pw_uid
|
||||||
|
raise PermissionError("The plugin loader does not seem to be hosted by any known user.")
|
||||||
|
|
||||||
|
# Get the user hosting the plugin loader
|
||||||
|
def _get_user() -> str:
|
||||||
|
return pwd.getpwuid(_get_user_id()).pw_name
|
||||||
|
|
||||||
|
# Get the effective user id of the running process
|
||||||
|
def _get_effective_user_id() -> int:
|
||||||
|
return os.geteuid()
|
||||||
|
|
||||||
|
# Get the effective user of the running process
|
||||||
|
def _get_effective_user() -> str:
|
||||||
|
return pwd.getpwuid(_get_effective_user_id()).pw_name
|
||||||
|
|
||||||
|
# Get the effective user group id of the running process
|
||||||
|
def _get_effective_user_group_id() -> int:
|
||||||
|
return os.getegid()
|
||||||
|
|
||||||
|
# Get the effective user group of the running process
|
||||||
|
def _get_effective_user_group() -> str:
|
||||||
|
return grp.getgrgid(_get_effective_user_group_id()).gr_name
|
||||||
|
|
||||||
|
# Get the user owner of the given file path.
|
||||||
|
def _get_user_owner(file_path) -> str:
|
||||||
|
return pwd.getpwuid(os.stat(file_path).st_uid).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
|
||||||
|
|
||||||
|
# Get the group id of the user hosting the plugin loader
|
||||||
|
def _get_user_group_id() -> int:
|
||||||
|
return pwd.getpwuid(_get_user_id()).pw_gid
|
||||||
|
|
||||||
|
# Get the group of the user hosting the plugin loader
|
||||||
|
def _get_user_group() -> str:
|
||||||
|
return grp.getgrgid(_get_user_group_id()).gr_name
|
||||||
|
|
||||||
|
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||||
|
user_str = ""
|
||||||
|
|
||||||
|
if (user == UserType.HOST_USER):
|
||||||
|
user_str = _get_user()+":"+_get_user_group()
|
||||||
|
elif (user == UserType.EFFECTIVE_USER):
|
||||||
|
user_str = _get_effective_user()+":"+_get_effective_user_group()
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown User Type")
|
||||||
|
|
||||||
|
result = call(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path])
|
||||||
|
return result == 0
|
||||||
|
|
||||||
|
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||||
|
result = call(["chmod", "-R", str(permissions), path] if recursive else ["chmod", str(permissions), path])
|
||||||
|
return result == 0
|
||||||
|
|
||||||
|
def folder_owner(path : str) -> UserType|None:
|
||||||
|
user_owner = _get_user_owner(path)
|
||||||
|
|
||||||
|
if (user_owner == _get_user()):
|
||||||
|
return UserType.HOST_USER
|
||||||
|
|
||||||
|
elif (user_owner == _get_effective_user()):
|
||||||
|
return UserType.EFFECTIVE_USER
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||||
|
user_name = "root"
|
||||||
|
|
||||||
|
if user == UserType.HOST_USER:
|
||||||
|
user_name = _get_user()
|
||||||
|
elif user == UserType.EFFECTIVE_USER:
|
||||||
|
user_name = _get_effective_user()
|
||||||
|
elif user == UserType.ROOT:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown User Type")
|
||||||
|
|
||||||
|
return pwd.getpwnam(user_name).pw_dir
|
||||||
|
|
||||||
|
def get_username() -> str:
|
||||||
|
return _get_user()
|
||||||
|
|
||||||
|
def setgid(user : UserType = UserType.HOST_USER):
|
||||||
|
user_id = 0
|
||||||
|
|
||||||
|
if user == UserType.HOST_USER:
|
||||||
|
user_id = _get_user_group_id()
|
||||||
|
elif user == UserType.ROOT:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown user type")
|
||||||
|
|
||||||
|
os.setgid(user_id)
|
||||||
|
|
||||||
|
def setuid(user : UserType = UserType.HOST_USER):
|
||||||
|
user_id = 0
|
||||||
|
|
||||||
|
if user == UserType.HOST_USER:
|
||||||
|
user_id = _get_user_id()
|
||||||
|
elif user == UserType.ROOT:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown user type")
|
||||||
|
|
||||||
|
os.setuid(user_id)
|
||||||
|
|
||||||
|
async def service_active(service_name : str) -> bool:
|
||||||
|
res = run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
|
||||||
|
return res.returncode == 0
|
||||||
|
|
||||||
|
async def service_restart(service_name : str) -> bool:
|
||||||
|
call(["systemctl", "daemon-reload"])
|
||||||
|
cmd = ["systemctl", "restart", service_name]
|
||||||
|
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||||
|
return res.returncode == 0
|
||||||
|
|
||||||
|
async def service_stop(service_name : str) -> bool:
|
||||||
|
cmd = ["systemctl", "stop", service_name]
|
||||||
|
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||||
|
return res.returncode == 0
|
||||||
|
|
||||||
|
async def service_start(service_name : str) -> bool:
|
||||||
|
cmd = ["systemctl", "start", service_name]
|
||||||
|
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||||
|
return res.returncode == 0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from customtypes import UserType
|
||||||
|
import os, sys
|
||||||
|
|
||||||
|
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||||
|
return True # Stubbed
|
||||||
|
|
||||||
|
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||||
|
return True # Stubbed
|
||||||
|
|
||||||
|
def folder_owner(path : str) -> UserType|None:
|
||||||
|
return UserType.HOST_USER # Stubbed
|
||||||
|
|
||||||
|
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||||
|
return os.path.expanduser("~") # Mostly stubbed
|
||||||
|
|
||||||
|
def setgid(user : UserType = UserType.HOST_USER):
|
||||||
|
pass # Stubbed
|
||||||
|
|
||||||
|
def setuid(user : UserType = UserType.HOST_USER):
|
||||||
|
pass # Stubbed
|
||||||
|
|
||||||
|
async def service_active(service_name : str) -> bool:
|
||||||
|
return True # Stubbed
|
||||||
|
|
||||||
|
async def service_stop(service_name : str) -> bool:
|
||||||
|
return True # Stubbed
|
||||||
|
|
||||||
|
async def service_start(service_name : str) -> bool:
|
||||||
|
return True # Stubbed
|
||||||
|
|
||||||
|
async def service_restart(service_name : str) -> bool:
|
||||||
|
if service_name == "plugin_loader":
|
||||||
|
sys.exit(42)
|
||||||
|
|
||||||
|
return True # Stubbed
|
||||||
|
|
||||||
|
def get_username() -> str:
|
||||||
|
return os.getlogin()
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import asyncio, time, random
|
||||||
|
from localplatform import ON_WINDOWS
|
||||||
|
|
||||||
|
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||||
|
|
||||||
|
class UnixSocket:
|
||||||
|
def __init__(self, on_new_message):
|
||||||
|
'''
|
||||||
|
on_new_message takes 1 string argument.
|
||||||
|
It's return value gets used, if not None, to write data to the socket.
|
||||||
|
Method should be async
|
||||||
|
'''
|
||||||
|
self.socket_addr = f"/tmp/plugin_socket_{time.time()}"
|
||||||
|
self.on_new_message = on_new_message
|
||||||
|
self.socket = None
|
||||||
|
self.reader = None
|
||||||
|
self.writer = None
|
||||||
|
|
||||||
|
async def setup_server(self):
|
||||||
|
self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||||
|
|
||||||
|
async def _open_socket_if_not_exists(self):
|
||||||
|
if not self.reader:
|
||||||
|
retries = 0
|
||||||
|
while retries < 10:
|
||||||
|
try:
|
||||||
|
self.reader, self.writer = await asyncio.open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
retries += 1
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def get_socket_connection(self):
|
||||||
|
if not await self._open_socket_if_not_exists():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return self.reader, self.writer
|
||||||
|
|
||||||
|
async def close_socket_connection(self):
|
||||||
|
if self.writer != None:
|
||||||
|
self.writer.close()
|
||||||
|
|
||||||
|
self.reader = None
|
||||||
|
|
||||||
|
async def read_single_line(self) -> str|None:
|
||||||
|
reader, writer = await self.get_socket_connection()
|
||||||
|
|
||||||
|
if self.reader == None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return await self._read_single_line(reader)
|
||||||
|
|
||||||
|
async def write_single_line(self, message : str):
|
||||||
|
reader, writer = await self.get_socket_connection()
|
||||||
|
|
||||||
|
if self.writer == None:
|
||||||
|
return;
|
||||||
|
|
||||||
|
await self._write_single_line(writer, message)
|
||||||
|
|
||||||
|
async def _read_single_line(self, reader) -> str:
|
||||||
|
line = bytearray()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line.extend(await reader.readuntil())
|
||||||
|
except asyncio.LimitOverrunError:
|
||||||
|
line.extend(await reader.read(reader._limit))
|
||||||
|
continue
|
||||||
|
except asyncio.IncompleteReadError as err:
|
||||||
|
line.extend(err.partial)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return line.decode("utf-8")
|
||||||
|
|
||||||
|
async def _write_single_line(self, writer, message : str):
|
||||||
|
if not message.endswith("\n"):
|
||||||
|
message += "\n"
|
||||||
|
|
||||||
|
writer.write(message.encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
async def _listen_for_method_call(self, reader, writer):
|
||||||
|
while True:
|
||||||
|
line = await self._read_single_line(reader)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = await self.on_new_message(line)
|
||||||
|
except Exception as e:
|
||||||
|
return
|
||||||
|
|
||||||
|
if res != None:
|
||||||
|
await self._write_single_line(writer, res)
|
||||||
|
|
||||||
|
class PortSocket (UnixSocket):
|
||||||
|
def __init__(self, on_new_message):
|
||||||
|
'''
|
||||||
|
on_new_message takes 1 string argument.
|
||||||
|
It's return value gets used, if not None, to write data to the socket.
|
||||||
|
Method should be async
|
||||||
|
'''
|
||||||
|
super().__init__(on_new_message)
|
||||||
|
self.host = "127.0.0.1"
|
||||||
|
self.port = random.sample(range(40000, 60000), 1)[0]
|
||||||
|
|
||||||
|
async def setup_server(self):
|
||||||
|
self.socket = await asyncio.start_server(self._listen_for_method_call, host=self.host, port=self.port, limit=BUFFER_LIMIT)
|
||||||
|
|
||||||
|
async def _open_socket_if_not_exists(self):
|
||||||
|
if not self.reader:
|
||||||
|
retries = 0
|
||||||
|
while retries < 10:
|
||||||
|
try:
|
||||||
|
self.reader, self.writer = await asyncio.open_connection(host=self.host, port=self.port, limit=BUFFER_LIMIT)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
retries += 1
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ON_WINDOWS:
|
||||||
|
class LocalSocket (PortSocket):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
class LocalSocket (UnixSocket):
|
||||||
|
pass
|
||||||
+28
-13
@@ -1,14 +1,15 @@
|
|||||||
# Change PyInstaller files permissions
|
# Change PyInstaller files permissions
|
||||||
import sys
|
import sys
|
||||||
from subprocess import call
|
from localplatform import chmod, chown, service_stop, service_start, ON_WINDOWS
|
||||||
if hasattr(sys, '_MEIPASS'):
|
if hasattr(sys, '_MEIPASS'):
|
||||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
chmod(sys._MEIPASS, 755)
|
||||||
# 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 json import dumps, loads
|
||||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||||
from os import getenv, chmod, path
|
from os import getenv, path
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
import aiohttp_cors
|
import aiohttp_cors
|
||||||
# Partial imports
|
# Partial imports
|
||||||
@@ -19,16 +20,15 @@ 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 (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||||
get_home_path, get_homebrew_path, get_user, get_user_group,
|
get_homebrew_path, mkdir_as_user, get_system_pythonpaths)
|
||||||
stop_systemd_unit, start_systemd_unit)
|
|
||||||
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
|
from injector import get_gamepadui_tab, Tab, get_tabs, 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
|
||||||
from utilities import Utilities
|
from utilities import Utilities
|
||||||
|
from customtypes import UserType
|
||||||
|
|
||||||
USER = get_user()
|
|
||||||
GROUP = get_user_group()
|
|
||||||
HOMEBREW_PATH = get_homebrew_path()
|
HOMEBREW_PATH = get_homebrew_path()
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
|
"plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
|
||||||
@@ -49,10 +49,11 @@ basicConfig(
|
|||||||
logger = getLogger("Main")
|
logger = getLogger("Main")
|
||||||
|
|
||||||
def chown_plugin_dir():
|
def chown_plugin_dir():
|
||||||
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
|
if not path.exists(CONFIG["plugin_path"]): # For safety, create the folder before attempting to do anything with it
|
||||||
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
mkdir_as_user(CONFIG["plugin_path"])
|
||||||
if code_chown != 0 or code_chmod != 0:
|
|
||||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
if not chown(CONFIG["plugin_path"], UserType.HOST_USER) or not chmod(CONFIG["plugin_path"], 555):
|
||||||
|
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||||
|
|
||||||
if CONFIG["chown_plugin_path"] == True:
|
if CONFIG["chown_plugin_path"] == True:
|
||||||
chown_plugin_dir()
|
chown_plugin_dir()
|
||||||
@@ -79,9 +80,9 @@ class PluginManager:
|
|||||||
|
|
||||||
async def startup(_):
|
async def startup(_):
|
||||||
if self.settings.getSetting("cef_forward", False):
|
if self.settings.getSetting("cef_forward", False):
|
||||||
self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
|
||||||
else:
|
else:
|
||||||
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
|
||||||
self.loop.create_task(self.loader_reinjector())
|
self.loop.create_task(self.loader_reinjector())
|
||||||
self.loop.create_task(self.load_plugins())
|
self.loop.create_task(self.load_plugins())
|
||||||
|
|
||||||
@@ -173,6 +174,20 @@ class PluginManager:
|
|||||||
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__":
|
||||||
|
if ON_WINDOWS:
|
||||||
|
# Fix windows/flask not recognising that .js means 'application/javascript'
|
||||||
|
import mimetypes
|
||||||
|
mimetypes.add_type('application/javascript', '.js')
|
||||||
|
|
||||||
|
# Required for multiprocessing support in frozen files
|
||||||
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
|
# Append the loader's plugin path to the recognized python paths
|
||||||
|
sys.path.append(path.join(path.dirname(__file__), "plugin"))
|
||||||
|
|
||||||
|
# Append the system and user python paths
|
||||||
|
sys.path.extend(get_system_pythonpaths())
|
||||||
|
|
||||||
loop = new_event_loop()
|
loop = new_event_loop()
|
||||||
set_event_loop(loop)
|
set_event_loop(loop)
|
||||||
PluginManager(loop).run()
|
PluginManager(loop).run()
|
||||||
|
|||||||
+30
-77
@@ -1,32 +1,27 @@
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||||
open_unix_connection, set_event_loop, sleep,
|
set_event_loop, sleep)
|
||||||
start_unix_server, IncompleteReadError, LimitOverrunError)
|
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
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
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from os import path, setgid, setuid, environ
|
from os import path, environ
|
||||||
from signal import SIGINT, signal
|
from signal import SIGINT, signal
|
||||||
from sys import exit, path as syspath
|
from sys import exit, path as syspath
|
||||||
from time import time
|
from time import time
|
||||||
|
from localsocket import LocalSocket
|
||||||
|
from localplatform import setgid, setuid, get_username, get_home_path
|
||||||
|
from customtypes import UserType
|
||||||
import helpers
|
import helpers
|
||||||
from updater import Updater
|
|
||||||
|
|
||||||
multiprocessing.set_start_method("fork")
|
|
||||||
|
|
||||||
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:
|
||||||
self.file = file
|
self.file = file
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
self.plugin_directory = plugin_directory
|
self.plugin_directory = plugin_directory
|
||||||
self.reader = None
|
|
||||||
self.writer = None
|
|
||||||
self.socket_addr = f"/tmp/plugin_socket_{time()}"
|
|
||||||
self.method_call_lock = Lock()
|
self.method_call_lock = Lock()
|
||||||
|
self.socket = LocalSocket(self._on_new_message)
|
||||||
|
|
||||||
self.version = None
|
self.version = None
|
||||||
|
|
||||||
@@ -35,7 +30,6 @@ class PluginWrapper:
|
|||||||
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 ""
|
||||||
@@ -59,13 +53,13 @@ class PluginWrapper:
|
|||||||
set_event_loop(new_event_loop())
|
set_event_loop(new_event_loop())
|
||||||
if self.passive:
|
if self.passive:
|
||||||
return
|
return
|
||||||
setgid(0 if "root" in self.flags else helpers.get_user_group_id())
|
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||||
setuid(0 if "root" in self.flags else helpers.get_user_id())
|
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||||
# 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"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||||
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
|
environ["USER"] = "root" if "root" in self.flags else get_username()
|
||||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||||
environ["DECKY_USER"] = helpers.get_user()
|
environ["DECKY_USER"] = get_username()
|
||||||
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
||||||
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)
|
||||||
@@ -78,12 +72,10 @@ class PluginWrapper:
|
|||||||
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
|
||||||
# append the loader's plugin path to the recognized python paths
|
|
||||||
syspath.append(path.realpath(path.join(path.dirname(__file__), "plugin")))
|
|
||||||
# append the plugin's `py_modules` to the recognized python paths
|
# append the plugin's `py_modules` to the recognized python paths
|
||||||
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
||||||
# append the system and user python paths
|
|
||||||
syspath.extend(helpers.get_system_pythonpaths())
|
|
||||||
spec = spec_from_file_location("_", self.file)
|
spec = spec_from_file_location("_", self.file)
|
||||||
module = module_from_spec(spec)
|
module = module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
@@ -93,7 +85,7 @@ class PluginWrapper:
|
|||||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||||
if hasattr(self.Plugin, "_main"):
|
if hasattr(self.Plugin, "_main"):
|
||||||
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.socket.setup_server())
|
||||||
get_event_loop().run_forever()
|
get_event_loop().run_forever()
|
||||||
except:
|
except:
|
||||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||||
@@ -111,24 +103,9 @@ class PluginWrapper:
|
|||||||
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 _on_new_message(self, message : str) -> str|None:
|
||||||
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
data = loads(message)
|
||||||
|
|
||||||
async def _listen_for_method_call(self, reader, writer):
|
|
||||||
while True:
|
|
||||||
line = bytearray()
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
line.extend(await reader.readuntil())
|
|
||||||
except LimitOverrunError:
|
|
||||||
line.extend(await reader.read(reader._limit))
|
|
||||||
continue
|
|
||||||
except IncompleteReadError as err:
|
|
||||||
line.extend(err.partial)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
data = loads(line.decode("utf-8"))
|
|
||||||
if "stop" in data:
|
if "stop" in data:
|
||||||
self.log.info("Calling Loader unload function.")
|
self.log.info("Calling Loader unload function.")
|
||||||
await self._unload()
|
await self._unload()
|
||||||
@@ -136,7 +113,8 @@ class PluginWrapper:
|
|||||||
while get_event_loop().is_running():
|
while get_event_loop().is_running():
|
||||||
await sleep(0)
|
await sleep(0)
|
||||||
get_event_loop().close()
|
get_event_loop().close()
|
||||||
return
|
raise Exception("Closing message listener")
|
||||||
|
|
||||||
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"])
|
||||||
@@ -144,22 +122,7 @@ class PluginWrapper:
|
|||||||
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"))
|
return dumps(d, ensure_ascii=False)
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
async def _open_socket_if_not_exists(self):
|
|
||||||
if not self.reader:
|
|
||||||
retries = 0
|
|
||||||
while retries < 10:
|
|
||||||
try:
|
|
||||||
self.reader, self.writer = await open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
await sleep(2)
|
|
||||||
retries += 1
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.passive:
|
if self.passive:
|
||||||
@@ -170,34 +133,24 @@ 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():
|
await self.socket.write_single_line(dumps({ "stop": True }, ensure_ascii=False))
|
||||||
self.writer.write((dumps({ "stop": True }, ensure_ascii=False)+"\n").encode("utf-8"))
|
await self.socket.close_socket_connection()
|
||||||
await self.writer.drain()
|
|
||||||
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():
|
reader, writer = await self.socket.get_socket_connection()
|
||||||
self.writer.write(
|
|
||||||
(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False) + "\n").encode("utf-8"))
|
await self.socket.write_single_line(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False))
|
||||||
await self.writer.drain()
|
|
||||||
line = bytearray()
|
line = await self.socket.read_single_line()
|
||||||
while True:
|
if line != None:
|
||||||
try:
|
res = loads(line)
|
||||||
line.extend(await self.reader.readuntil())
|
|
||||||
except LimitOverrunError:
|
|
||||||
line.extend(await self.reader.read(self.reader._limit))
|
|
||||||
continue
|
|
||||||
except IncompleteReadError as err:
|
|
||||||
line.extend(err.partial)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
res = loads(line.decode("utf-8"))
|
|
||||||
if not res["success"]:
|
if not res["success"]:
|
||||||
raise Exception(res["res"])
|
raise Exception(res["res"])
|
||||||
return res["res"]
|
return res["res"]
|
||||||
+7
-7
@@ -1,14 +1,13 @@
|
|||||||
from json import dump, load
|
from json import dump, load
|
||||||
from os import mkdir, path, listdir, rename
|
from os import mkdir, path, listdir, rename
|
||||||
from shutil import chown
|
from localplatform import chown, folder_owner
|
||||||
|
from customtypes import UserType
|
||||||
|
|
||||||
from helpers import get_home_path, get_homebrew_path, get_user, get_user_group, get_user_owner
|
from helpers import get_homebrew_path
|
||||||
|
|
||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
def __init__(self, name, settings_directory = None) -> None:
|
def __init__(self, name, settings_directory = None) -> None:
|
||||||
USER = get_user()
|
|
||||||
GROUP = get_user_group()
|
|
||||||
wrong_dir = get_homebrew_path()
|
wrong_dir = get_homebrew_path()
|
||||||
if settings_directory == None:
|
if settings_directory == None:
|
||||||
settings_directory = path.join(wrong_dir, "settings")
|
settings_directory = path.join(wrong_dir, "settings")
|
||||||
@@ -18,7 +17,7 @@ class SettingsManager:
|
|||||||
#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)
|
||||||
|
|
||||||
#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):
|
||||||
@@ -29,8 +28,9 @@ class SettingsManager:
|
|||||||
|
|
||||||
|
|
||||||
#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:
|
|
||||||
chown(settings_directory, USER, GROUP)
|
if folder_owner(settings_directory) != UserType.HOST_USER:
|
||||||
|
chown(settings_directory, UserType.HOST_USER, False)
|
||||||
|
|
||||||
self.settings = {}
|
self.settings = {}
|
||||||
|
|
||||||
|
|||||||
+21
-11
@@ -6,7 +6,7 @@ from ensurepip import version
|
|||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import getcwd, path, remove
|
from os import getcwd, path, remove
|
||||||
from subprocess import call
|
from localplatform import chmod, service_restart, ON_LINUX
|
||||||
|
|
||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession, web
|
||||||
|
|
||||||
@@ -132,13 +132,25 @@ class Updater:
|
|||||||
async def do_update(self):
|
async def do_update(self):
|
||||||
logger.debug("Starting update.")
|
logger.debug("Starting update.")
|
||||||
version = self.remoteVer["tag_name"]
|
version = self.remoteVer["tag_name"]
|
||||||
download_url = self.remoteVer["assets"][0]["browser_download_url"]
|
download_url = None
|
||||||
|
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||||
|
download_temp_filename = download_filename + ".new"
|
||||||
|
|
||||||
|
for x in self.remoteVer["assets"]:
|
||||||
|
if x["name"] == download_filename:
|
||||||
|
download_url = x["browser_download_url"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if download_url == None:
|
||||||
|
raise Exception("Download url not found")
|
||||||
|
|
||||||
service_url = self.get_service_url()
|
service_url = self.get_service_url()
|
||||||
logger.debug("Retrieved service URL")
|
logger.debug("Retrieved service URL")
|
||||||
|
|
||||||
tab = await get_gamepadui_tab()
|
tab = await get_gamepadui_tab()
|
||||||
await tab.open_websocket()
|
await tab.open_websocket()
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
|
if ON_LINUX:
|
||||||
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:
|
||||||
@@ -167,12 +179,7 @@ class Updater:
|
|||||||
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("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||||
total = int(res.headers.get('content-length', 0))
|
total = int(res.headers.get('content-length', 0))
|
||||||
# we need to not delete the binary until we have downloaded the new binary!
|
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||||
try:
|
|
||||||
remove(path.join(getcwd(), "PluginLoader"))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
|
|
||||||
progress = 0
|
progress = 0
|
||||||
raw = 0
|
raw = 0
|
||||||
async for c in res.content.iter_chunked(512):
|
async for c in res.content.iter_chunked(512):
|
||||||
@@ -186,12 +193,15 @@ class Updater:
|
|||||||
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")])
|
if ON_LINUX:
|
||||||
|
remove(path.join(getcwd(), download_filename))
|
||||||
|
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
|
||||||
|
chmod(path.join(getcwd(), download_filename), 777, False)
|
||||||
|
|
||||||
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()
|
||||||
await tab.close_websocket()
|
await tab.close_websocket()
|
||||||
|
|
||||||
async def do_restart(self):
|
async def do_restart(self):
|
||||||
call(["systemctl", "daemon-reload"])
|
await service_restart("plugin_loader")
|
||||||
call(["systemctl", "restart", "plugin_loader"])
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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
|
import subprocess
|
||||||
|
from localplatform import service_stop, service_start
|
||||||
|
|
||||||
class Utilities:
|
class Utilities:
|
||||||
def __init__(self, context) -> None:
|
def __init__(self, context) -> None:
|
||||||
@@ -174,11 +174,11 @@ class Utilities:
|
|||||||
return self.context.settings.setSetting(key, value)
|
return self.context.settings.setSetting(key, value)
|
||||||
|
|
||||||
async def allow_remote_debugging(self):
|
async def allow_remote_debugging(self):
|
||||||
await helpers.start_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
await service_start(helpers.REMOTE_DEBUGGER_UNIT)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def disallow_remote_debugging(self):
|
async def disallow_remote_debugging(self):
|
||||||
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def filepicker_ls(self, path, include_files=True):
|
async def filepicker_ls(self, path, include_files=True):
|
||||||
|
|||||||
Reference in New Issue
Block a user