mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-30 06:59:13 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f41eb93ef | |||
| 670ae7d8a7 | |||
| 01ec1663bc | |||
| b840d75cb8 | |||
| e22ba20d13 | |||
| bff410d98b | |||
| 7188db9877 | |||
| 02e8640ad4 |
@@ -41,7 +41,7 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Run prettier (TypeScript)
|
||||
working-directory: frontend
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Run pyright (Python)
|
||||
uses: jakebailey/pyright-action@v1
|
||||
|
||||
@@ -95,7 +95,7 @@ Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/e
|
||||
1. In your clone of the repository, run these commands.
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm i
|
||||
pnpm i # NOTE: you may need to approve esbuild's build script with pnpm approve-builds
|
||||
pnpm run build
|
||||
```
|
||||
1. If you are modifying the UI, these commands will need to be run before deploying the changes to your Steam Deck.
|
||||
|
||||
@@ -18,9 +18,10 @@ from enum import IntEnum
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
# Local modules
|
||||
from .localplatform.localplatform import chown, chmod
|
||||
from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
|
||||
from .loader import Loader, Plugins
|
||||
from .helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from .enums import UserType
|
||||
from .settings import SettingsManager
|
||||
|
||||
logger = getLogger("Browser")
|
||||
@@ -60,13 +61,6 @@ class PluginBrowser:
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
|
||||
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
|
||||
@@ -101,8 +95,6 @@ class PluginBrowser:
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
chown(self.plugin_path)
|
||||
chmod(pluginBasePath, 555)
|
||||
else:
|
||||
rv = True
|
||||
logger.info(f"No Remote Binaries to Download")
|
||||
@@ -124,6 +116,25 @@ class PluginBrowser:
|
||||
return folder
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
|
||||
plugin_json_path = path.join(plugin_dir, 'plugin.json')
|
||||
logger.debug(f"Checking plugin.json at {plugin_json_path}")
|
||||
|
||||
root_plugin = False
|
||||
|
||||
if access(plugin_json_path, R_OK):
|
||||
with open(plugin_json_path, "r", encoding="utf-8") as f:
|
||||
plugin_json = json.load(f)
|
||||
if "flags" in plugin_json and "root" in plugin_json["flags"]:
|
||||
root_plugin = True
|
||||
|
||||
logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
|
||||
if get_chown_plugin_path():
|
||||
return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
|
||||
else:
|
||||
logger.debug("chown disabled by environment")
|
||||
return True
|
||||
|
||||
async def uninstall_plugin(self, name: str):
|
||||
if self.loader.watcher:
|
||||
@@ -266,6 +277,7 @@ class PluginBrowser:
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
chown_ret = self.set_plugin_dir_permissions(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
@@ -278,6 +290,9 @@ class PluginBrowser:
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
elif not chown_ret:
|
||||
logger.error("Could not chown plugin")
|
||||
return
|
||||
else:
|
||||
logger.error("Could not download remote binaries")
|
||||
return
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from enum import IntEnum
|
||||
|
||||
class UserType(IntEnum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
HOST_USER = 1 # usually deck
|
||||
EFFECTIVE_USER = 2 # usually root
|
||||
|
||||
class PluginLoadType(IntEnum):
|
||||
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
|
||||
|
||||
@@ -181,7 +181,8 @@ def get_user_group_id() -> int:
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username: str | None = None) -> str:
|
||||
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
|
||||
# TODO hardcoded root is kinda a hack
|
||||
return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
return await localplatform.service_active(unit_name)
|
||||
|
||||
@@ -59,8 +59,6 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
|
||||
user_str = _get_user()+":"+_get_user_group()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_str = _get_effective_user()+":"+_get_effective_user_group()
|
||||
elif user == UserType.ROOT:
|
||||
user_str = "root:root"
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
@@ -87,7 +85,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
|
||||
return True
|
||||
|
||||
def folder_owner(path : str) -> UserType|None:
|
||||
def file_owner(path : str) -> UserType|None:
|
||||
user_owner = _get_user_owner(path)
|
||||
|
||||
if (user_owner == _get_user()):
|
||||
@@ -106,13 +104,14 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
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_effective_username() -> str:
|
||||
return _get_effective_user()
|
||||
|
||||
def get_username() -> str:
|
||||
return _get_user()
|
||||
|
||||
@@ -121,8 +120,8 @@ def setgid(user : UserType = UserType.HOST_USER):
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_group_id()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
pass # we already are
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
@@ -133,8 +132,8 @@ def setuid(user : UserType = UserType.HOST_USER):
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_id()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
pass # we already are
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def folder_owner(path : str) -> UserType|None:
|
||||
def file_owner(path : str) -> UserType|None:
|
||||
return UserType.HOST_USER # Stubbed
|
||||
|
||||
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
@@ -34,6 +34,9 @@ async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
|
||||
return True # Stubbed
|
||||
|
||||
def get_effective_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
def get_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def chown_plugin_dir():
|
||||
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
|
||||
mkdir_as_user(plugin_path)
|
||||
|
||||
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
|
||||
if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if get_chown_plugin_path() == True:
|
||||
|
||||
@@ -8,7 +8,8 @@ from traceback import format_exc
|
||||
|
||||
from .sandboxed_plugin import SandboxedPlugin
|
||||
from .messages import MethodCallRequest, SocketMessageType
|
||||
from ..enums import PluginLoadType
|
||||
from ..enums import PluginLoadType, UserType
|
||||
from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..helpers import get_homebrew_path, mkdir_as_user
|
||||
|
||||
@@ -26,9 +27,12 @@ class PluginWrapper:
|
||||
|
||||
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
|
||||
|
||||
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")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
plugin_dir_path = path.join(plugin_path, plugin_directory)
|
||||
plugin_json_path = path.join(plugin_dir_path, "plugin.json")
|
||||
|
||||
json = load(open(plugin_json_path, "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_dir_path, "package.json")):
|
||||
package_json = load(open(path.join(plugin_dir_path, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
if ("type" in package_json and package_json["type"] == "module"):
|
||||
self.load_type = PluginLoadType.ESMODULE_V1.value
|
||||
@@ -42,6 +46,17 @@ class PluginWrapper:
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
if get_chown_plugin_path():
|
||||
# ensure plugin folder ownership
|
||||
if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
|
||||
chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
|
||||
chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
|
||||
chmod(plugin_dir_path, 755, True)
|
||||
# fix plugin.json permissions
|
||||
if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
|
||||
chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
|
||||
chmod(plugin_json_path, 755, False)
|
||||
|
||||
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
|
||||
self.proc: Process | None = None
|
||||
self._socket = LocalSocket()
|
||||
|
||||
@@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
|
||||
from ..enums import UserType
|
||||
from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport]
|
||||
from .. import helpers
|
||||
|
||||
from typing import List, TypeVar, Any
|
||||
|
||||
@@ -61,10 +61,10 @@ class SandboxedPlugin:
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
# export a bunch of environment variables to help plugin developers
|
||||
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["USER"] = "root" if "root" in self.flags else get_username()
|
||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||
environ["DECKY_USER"] = get_username()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from typing import Any, Dict
|
||||
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
|
||||
from .enums import UserType
|
||||
|
||||
from .helpers import get_homebrew_path
|
||||
@@ -28,8 +28,8 @@ class SettingsManager:
|
||||
|
||||
|
||||
#If the owner of the settings directory is not the user, then set it as the user:
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
|
||||
if folder_owner(settings_directory) != expected_user:
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
|
||||
if file_owner(settings_directory) != expected_user:
|
||||
chown(settings_directory, expected_user, False)
|
||||
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
@@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
|
||||
from typing import Callable, Coroutine, Dict, Any, cast
|
||||
|
||||
from traceback import format_exc
|
||||
|
||||
@@ -29,8 +29,6 @@ class WSMessageExtra(WSMessage):
|
||||
|
||||
# see wsrouter.ts for typings
|
||||
|
||||
DataType = TypeVar("DataType")
|
||||
|
||||
Route = Callable[..., Coroutine[Any, Any, Any]]
|
||||
|
||||
class WSRouter:
|
||||
|
||||
Generated
+732
-815
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ include = [
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<=3.13.5"
|
||||
python = ">=3.10,<3.14"
|
||||
|
||||
aiohttp = "^3.10.11"
|
||||
aiohttp-jinja2 = "^1.5.1"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@decky/ui": "^4.10.2",
|
||||
"@decky/ui": "^4.10.4",
|
||||
"compare-versions": "^6.1.1",
|
||||
"filesize": "^10.1.2",
|
||||
"i18next": "^23.11.5",
|
||||
|
||||
Generated
+5
-5
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@decky/ui':
|
||||
specifier: ^4.10.2
|
||||
version: 4.10.2
|
||||
specifier: ^4.10.4
|
||||
version: 4.10.4
|
||||
compare-versions:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
@@ -218,8 +218,8 @@ packages:
|
||||
'@decky/api@1.1.1':
|
||||
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
|
||||
|
||||
'@decky/ui@4.10.2':
|
||||
resolution: {integrity: sha512-dfY/OEI/rhG4d3Tvx4Y3TLmBrJ+nAm2RTbkoOJ3VAglql3Lu7RY2ixeDFbs21ZWWzWh/C+9dAQKjQ4lx4d1f2g==}
|
||||
'@decky/ui@4.10.4':
|
||||
resolution: {integrity: sha512-swgC4IVtQzZVw8dtP/iztpNYUl1eR0dxWfiMpswY8YglDsBn4ntspbL91Ic4WgxvkOEMSpsIs+zkVtjHE9zi3A==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
|
||||
@@ -2295,7 +2295,7 @@ snapshots:
|
||||
|
||||
'@decky/api@1.1.1': {}
|
||||
|
||||
'@decky/ui@4.10.2': {}
|
||||
'@decky/ui@4.10.4': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
optional: true
|
||||
|
||||
+12
-5
@@ -20,7 +20,10 @@ if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
required_dependencies=(websocat jq curl chromium)
|
||||
[[ -f "$HOME/.config/deckdebug/config.sh" ]] && source "$HOME/.config/deckdebug/config.sh"
|
||||
CHROMIUM="${CHROMIUM:-chromium}"
|
||||
|
||||
required_dependencies=(websocat jq curl $CHROMIUM)
|
||||
|
||||
# Check if the dependencies are installed
|
||||
for cmd in "${required_dependencies[@]}"; do
|
||||
@@ -30,7 +33,7 @@ for cmd in "${required_dependencies[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
chromium --remote-debugging-port=9222 &
|
||||
$CHROMIUM --remote-debugging-port=9222 &
|
||||
sleep 2
|
||||
|
||||
ADDR=$1
|
||||
@@ -49,10 +52,14 @@ while :; do
|
||||
TARGET=$NEWTARGET
|
||||
TARGETURL="http://$ADDR/devtools/inspector.html?ws=$ADDR/devtools/page/$TARGET"
|
||||
|
||||
LOCALTARGET=$(echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
|
||||
echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
|
||||
{"id": 2, "method": "Target.closeTarget", "params": {"targetId": "'$LOCALTARGET'"}}' \
|
||||
| websocat ws://$LOCAL/devtools/page/$LOCALTARGET \
|
||||
| jq -r '.result.targetId')
|
||||
| websocat -t ws://$LOCAL/devtools/page/$LOCALTARGET
|
||||
|
||||
sleep 2
|
||||
|
||||
LOCALTARGETS=$(curl -s http://$LOCAL/json/list)
|
||||
LOCALTARGET=$(jq -r '.[] | select(.title | startswith("DevTools")) | .id' <<< "$LOCALTARGETS")
|
||||
|
||||
echo started devtools at $LOCALTARGET
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user