mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-04 08:49:54 +00:00
Compare commits
17 Commits
v3.0.0-pre11
...
v3.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1284075d02 | |||
| d2c5aef58b | |||
| caac379b08 | |||
| c487a6e15a | |||
| 9df5f00068 | |||
| 508408ad5a | |||
| ef4ca204bd | |||
| 1d7af36a2b | |||
| 6b78e0ae9c | |||
| 1f5d5f9f1a | |||
| f7a47127a7 | |||
| 494f8dac5e | |||
| bcc14848c5 | |||
| 0e40374b10 | |||
| 81ffe11106 | |||
| d06494885a | |||
| a6e4bcf052 |
@@ -57,18 +57,34 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Decky Loader Version
|
||||
description: Specify the exact version of Decky.
|
||||
placeholder: v3.0.0-pre12
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Plugin Info
|
||||
description: "Include all plugins installed including their version. Helpful script here: https://github.com/SteamDeckHomebrew/decky-loader/blob/main/scripts/plugin-info.sh"
|
||||
placeholder: "If you don't want to collect this info manually you can download a helpful script linked in this item's description and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Have you modified the read-only filesystem at any point?
|
||||
description: Describe how here, if you haven't done anything you can leave this blank
|
||||
placeholder: Yes, I've installed neofetch via pacman.
|
||||
description: "Describe how here, if you haven't done anything you can leave this blank"
|
||||
placeholder: "Yes, I've installed neofetch via pacman."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Backend Logs
|
||||
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
|
||||
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here.
|
||||
placeholder: deckylog.txt
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -36,10 +36,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v1
|
||||
uses: asdf-vm/actions/install@v3
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.3.0
|
||||
semver 3.4.0
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
from re import compile
|
||||
from asyncio import Lock
|
||||
from asyncio import Lock, create_subprocess_exec
|
||||
from asyncio.subprocess import PIPE, DEVNULL, STDOUT, Process
|
||||
from subprocess import call as call_sync
|
||||
import os, pwd, grp, sys, logging
|
||||
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||
from typing import IO, Any, Mapping
|
||||
from ..enums import UserType
|
||||
|
||||
logger = logging.getLogger("localplatform")
|
||||
|
||||
# subprocess._ENV
|
||||
ENV = Mapping[str, str]
|
||||
ProcessIO = int | IO[Any] | None
|
||||
async def run(args: list[str], stdin: ProcessIO = DEVNULL, stdout: ProcessIO = PIPE, stderr: ProcessIO = PIPE, env: ENV | None = None) -> tuple[Process, bytes | None, bytes | None]:
|
||||
proc = await create_subprocess_exec(args[0], *(args[1:]), stdin=stdin, stdout=stdout, stderr=stderr, env=env)
|
||||
proc_stdout, proc_stderr = await proc.communicate()
|
||||
return (proc, proc_stdout, proc_stderr)
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def _get_user_id() -> int:
|
||||
return pwd.getpwnam(_get_user()).pw_uid
|
||||
@@ -54,7 +64,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
result = call(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path])
|
||||
result = call_sync(["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:
|
||||
@@ -131,13 +141,17 @@ def setuid(user : UserType = UserType.HOST_USER):
|
||||
os.setuid(user_id)
|
||||
|
||||
async def service_active(service_name : str) -> bool:
|
||||
res = run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
|
||||
res, _, _ = await 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"])
|
||||
async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
await run(["systemctl", "daemon-reload"])
|
||||
cmd = ["systemctl", "restart", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
|
||||
if not block:
|
||||
cmd.append("--no-block")
|
||||
|
||||
res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_stop(service_name : str) -> bool:
|
||||
@@ -146,7 +160,7 @@ async def service_stop(service_name : str) -> bool:
|
||||
return True
|
||||
|
||||
cmd = ["systemctl", "stop", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_start(service_name : str) -> bool:
|
||||
@@ -155,13 +169,13 @@ async def service_start(service_name : str) -> bool:
|
||||
return True
|
||||
|
||||
cmd = ["systemctl", "start", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def restart_webhelper() -> bool:
|
||||
logger.info("Restarting steamwebhelper")
|
||||
# TODO move to pkill
|
||||
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
||||
res, _, _ = await run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
@@ -241,12 +255,12 @@ async def close_cef_socket():
|
||||
logger.warning("Can't close CEF socket as Decky isn't running as root.")
|
||||
return
|
||||
# Look for anything listening TCP on port 8080
|
||||
lsof = run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], capture_output=True, text=True)
|
||||
if lsof.returncode != 0 or len(lsof.stdout) < 1:
|
||||
lsof, stdout, _ = await run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], stdout=PIPE)
|
||||
if not stdout or lsof.returncode != 0 or len(stdout) < 1:
|
||||
logger.error(f"lsof call failed in close_cef_socket! return code: {str(lsof.returncode)}")
|
||||
return
|
||||
|
||||
lsof_data = cef_socket_lsof_regex.match(lsof.stdout)
|
||||
lsof_data = cef_socket_lsof_regex.match(stdout.decode())
|
||||
|
||||
if not lsof_data:
|
||||
logger.error("lsof regex match failed in close_cef_socket!")
|
||||
@@ -258,7 +272,7 @@ async def close_cef_socket():
|
||||
logger.info(f"Closing CEF socket with PID {pid} and FD {fd}")
|
||||
|
||||
# Use gdb to inject a close() call for the socket fd into steamwebhelper
|
||||
gdb_ret = run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"], env={"LD_LIBRARY_PATH": ""})
|
||||
gdb_ret, _, _ = await run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"], env={"LD_LIBRARY_PATH": ""})
|
||||
|
||||
if gdb_ret.returncode != 0:
|
||||
logger.error(f"Failed to close CEF socket with gdb! return code: {str(gdb_ret.returncode)}", exc_info=True)
|
||||
|
||||
@@ -28,7 +28,7 @@ async def service_stop(service_name : str) -> bool:
|
||||
async def service_start(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_restart(service_name : str) -> bool:
|
||||
async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
if service_name == "plugin_loader":
|
||||
sys.exit(42)
|
||||
|
||||
|
||||
@@ -138,16 +138,17 @@ class PluginManager:
|
||||
tasks = all_tasks()
|
||||
current = current_task()
|
||||
async def cancel_task(task: Task[Any]):
|
||||
logger.debug(f"Cancelling task {task}")
|
||||
name = task.get_coro().__qualname__
|
||||
logger.debug(f"Cancelling task {name}")
|
||||
try:
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except CancelledError:
|
||||
pass
|
||||
logger.debug(f"Task {task} finished")
|
||||
logger.debug(f"Task {name} finished")
|
||||
except:
|
||||
logger.warning(f"Failed to cancel task {task}:\n" + format_exc())
|
||||
logger.warning(f"Failed to cancel task {name}:\n" + format_exc())
|
||||
pass
|
||||
if current:
|
||||
tasks.remove(current)
|
||||
|
||||
@@ -120,18 +120,25 @@ class PluginWrapper:
|
||||
start_time = time()
|
||||
if self.passive:
|
||||
return
|
||||
self.log.info(f"Shutting down {self.name}")
|
||||
|
||||
_, pending = await wait([
|
||||
create_task(self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False)))
|
||||
], timeout=1)
|
||||
pending: set[Task[None]] | None = None;
|
||||
|
||||
if uninstall:
|
||||
_, pending = await wait([
|
||||
create_task(self._socket.write_single_line(dumps({ "uninstall": uninstall }, ensure_ascii=False)))
|
||||
], timeout=1)
|
||||
|
||||
self.terminate() # the plugin process will handle SIGTERM and shut down cleanly without a socket message
|
||||
|
||||
if hasattr(self, "_listener_task"):
|
||||
self._listener_task.cancel()
|
||||
|
||||
await self.kill_if_still_running()
|
||||
|
||||
for pending_task in pending:
|
||||
pending_task.cancel()
|
||||
if pending:
|
||||
for pending_task in pending:
|
||||
pending_task.cancel()
|
||||
|
||||
self.log.info(f"Plugin {self.name} has been stopped in {time() - start_time:.1f}s")
|
||||
except Exception as e:
|
||||
@@ -139,18 +146,14 @@ class PluginWrapper:
|
||||
|
||||
async def kill_if_still_running(self):
|
||||
start_time = time()
|
||||
sigtermed = False
|
||||
while self.proc and self.proc.is_alive():
|
||||
elapsed_time = time() - start_time
|
||||
if elapsed_time >= 5 and not sigtermed:
|
||||
sigtermed = True
|
||||
self.log.warning(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGTERM!")
|
||||
self.terminate()
|
||||
elif elapsed_time >= 10:
|
||||
self.log.warning(f"Plugin {self.name} still alive 10 seconds after stop request! Sending SIGKILL!")
|
||||
if elapsed_time >= 5:
|
||||
self.log.warning(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGKILL!")
|
||||
self.terminate(True)
|
||||
await sleep(0.1)
|
||||
|
||||
|
||||
def terminate(self, kill: bool = False):
|
||||
if self.proc and self.proc.is_alive():
|
||||
if kill:
|
||||
|
||||
@@ -4,8 +4,9 @@ from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from asyncio import (get_event_loop, new_event_loop,
|
||||
from asyncio import (ensure_future, get_event_loop, new_event_loop,
|
||||
set_event_loop)
|
||||
from signal import SIGINT, SIGTERM
|
||||
from setproctitle import setproctitle, setthreadtitle
|
||||
|
||||
from .messages import SocketResponseDict, SocketMessageType
|
||||
@@ -38,6 +39,8 @@ class SandboxedPlugin:
|
||||
self.version = version
|
||||
self.author = author
|
||||
self.api_version = api_version
|
||||
self.shutdown_running = False
|
||||
self.uninstalling = False
|
||||
|
||||
self.log = getLogger("sandboxed_plugin")
|
||||
|
||||
@@ -48,7 +51,11 @@ class SandboxedPlugin:
|
||||
setproctitle(f"{self.name} ({self.file})")
|
||||
setthreadtitle(self.name)
|
||||
|
||||
set_event_loop(new_event_loop())
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
# When running Decky manually in a terminal, ctrl-c will trigger this, so we have to handle it properly
|
||||
loop.add_signal_handler(SIGINT, lambda: ensure_future(self.shutdown()))
|
||||
loop.add_signal_handler(SIGTERM, lambda: ensure_future(self.shutdown()))
|
||||
if self.passive:
|
||||
return
|
||||
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
@@ -155,22 +162,27 @@ class SandboxedPlugin:
|
||||
self.log.error("Failed to uninstall " + self.name + "!\n" + format_exc())
|
||||
pass
|
||||
|
||||
async def on_new_message(self, message : str) -> str|None:
|
||||
data = loads(message)
|
||||
|
||||
if "stop" in data:
|
||||
async def shutdown(self):
|
||||
if not self.shutdown_running:
|
||||
self.shutdown_running = True
|
||||
self.log.info(f"Calling Loader unload function for {self.name}.")
|
||||
await self._unload()
|
||||
|
||||
if data.get('uninstall'):
|
||||
if self.uninstalling:
|
||||
self.log.info("Calling Loader uninstall function.")
|
||||
await self._uninstall()
|
||||
|
||||
self.log.debug("Stopping event loop")
|
||||
self.log.debug("Stopping event loop")
|
||||
|
||||
loop = get_event_loop()
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
sys.exit(0)
|
||||
loop = get_event_loop()
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
sys.exit(0)
|
||||
|
||||
async def on_new_message(self, message : str) -> str|None:
|
||||
data = loads(message)
|
||||
|
||||
if "uninstall" in data:
|
||||
self.uninstalling = data.get("uninstall")
|
||||
|
||||
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
|
||||
try:
|
||||
|
||||
@@ -24,6 +24,7 @@ logger = getLogger("Updater")
|
||||
|
||||
class RemoteVerAsset(TypedDict):
|
||||
name: str
|
||||
size: int
|
||||
browser_download_url: str
|
||||
class RemoteVer(TypedDict):
|
||||
tag_name: str
|
||||
@@ -198,11 +199,13 @@ class Updater:
|
||||
|
||||
version = self.remoteVer["tag_name"]
|
||||
download_url = None
|
||||
size_in_bytes = None
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
|
||||
for x in self.remoteVer["assets"]:
|
||||
if x["name"] == download_filename:
|
||||
download_url = x["browser_download_url"]
|
||||
size_in_bytes = x["size"]
|
||||
break
|
||||
|
||||
if download_url == None:
|
||||
@@ -238,10 +241,10 @@ class Updater:
|
||||
os.mkdir(path.join(getcwd(), ".systemd"))
|
||||
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
||||
|
||||
await self.download_decky_binary(download_url, version)
|
||||
await self.download_decky_binary(download_url, version, size_in_bytes=size_in_bytes)
|
||||
|
||||
async def do_restart(self):
|
||||
await service_restart("plugin_loader")
|
||||
await service_restart("plugin_loader", block=False)
|
||||
|
||||
async def do_shutdown(self):
|
||||
await service_stop("plugin_loader")
|
||||
|
||||
Vendored
+2
-69
@@ -1,70 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader pre-release..."
|
||||
|
||||
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
|
||||
# Create folder structure
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
|
||||
VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
|
||||
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
KillMode=process
|
||||
TimeoutStopSec=45
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" ]]; then
|
||||
printf "Grabbed latest prerelease service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest prerelease systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-prerelease.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
echo This script is deprecated! Use https://github.com/SteamDeckHomebrew/decky-installer/raw/main/cli/install_prerelease.sh instead!
|
||||
exit 1
|
||||
Vendored
+2
-69
@@ -1,70 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader release..."
|
||||
|
||||
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
|
||||
# Create folder structure
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
|
||||
VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
|
||||
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
KillMode=process
|
||||
TimeoutStopSec=45
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" ]]; then
|
||||
printf "Grabbed latest release service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-release.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest release systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-release.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-release.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
echo This script is deprecated! Use https://github.com/SteamDeckHomebrew/decky-installer/raw/main/cli/install_release.sh instead!
|
||||
exit 1
|
||||
+1
-1
@@ -6,7 +6,7 @@ Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
KillMode=process
|
||||
TimeoutStopSec=45
|
||||
TimeoutStopSec=15
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
|
||||
Vendored
+1
-1
@@ -6,7 +6,7 @@ Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
KillMode=process
|
||||
TimeoutStopSec=45
|
||||
TimeoutStopSec=15
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Navigation } from '@decky/ui';
|
||||
import { AnchorHTMLAttributes, FC } from 'react';
|
||||
|
||||
const ExternalLink: FC<AnchorHTMLAttributes<HTMLAnchorElement>> = (props) => {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onClick ? props.onClick(e) : props.href && Navigation.NavigateToExternalWeb(props.href);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalLink;
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { InstallType } from '../../plugin';
|
||||
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
|
||||
import ExternalLink from '../ExternalLink';
|
||||
|
||||
interface PluginCardProps {
|
||||
plugin: StorePlugin;
|
||||
@@ -108,7 +109,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
>
|
||||
<i>{t('PluginCard.plugin_full_access')}</i>{' '}
|
||||
<a
|
||||
<ExternalLink
|
||||
className="deckyStoreCardDescriptionRootLink"
|
||||
href="https://deckbrew.xyz/root"
|
||||
target="_blank"
|
||||
@@ -118,7 +119,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
>
|
||||
deckbrew.xyz/root
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import logo from '../../../assets/plugin_store.png';
|
||||
import Logger from '../../logger';
|
||||
import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store';
|
||||
import ExternalLink from '../ExternalLink';
|
||||
import PluginCard from './PluginCard';
|
||||
|
||||
const logger = new Logger('Store');
|
||||
@@ -207,7 +208,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
<h2 style={{ margin: 0 }}>{t('Store.store_testing_warning.label')}</h2>
|
||||
<span>
|
||||
{`${t('Store.store_testing_warning.desc')} `}
|
||||
<a
|
||||
<ExternalLink
|
||||
href="https://decky.xyz/testing"
|
||||
target="_blank"
|
||||
style={{
|
||||
@@ -215,7 +216,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
}}
|
||||
>
|
||||
decky.xyz/testing
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -269,7 +270,7 @@ const AboutTab: FC<{}> = () => {
|
||||
<span className="deckyStoreAboutHeader">Testing</span>
|
||||
<span>
|
||||
{t('Store.store_testing_cta')}{' '}
|
||||
<a
|
||||
<ExternalLink
|
||||
href="https://decky.xyz/testing"
|
||||
target="_blank"
|
||||
style={{
|
||||
@@ -277,7 +278,7 @@ const AboutTab: FC<{}> = () => {
|
||||
}}
|
||||
>
|
||||
decky.xyz/testing
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
|
||||
<span>{t('Store.store_contrib.desc')}</span>
|
||||
|
||||
@@ -16,8 +16,10 @@ interface Window {
|
||||
console.time('[Decky:Boot] Waiting for React root mount...');
|
||||
let root;
|
||||
while (
|
||||
// Does React root node exist?
|
||||
!(root = document.getElementById('root')) ||
|
||||
!(root as any)[Object.keys(root).find((k) => k.startsWith('__reactContainer$')) as string]
|
||||
// Does it have a child element?
|
||||
!(root as any)[Object.keys(root).find((k) => k.startsWith('__reactContainer$')) as string].child
|
||||
) {
|
||||
await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here.
|
||||
}
|
||||
@@ -25,6 +27,7 @@ interface Window {
|
||||
|
||||
if (!window.SP_REACT) {
|
||||
console.debug('[Decky:Boot] Setting up Webpack & React globals...');
|
||||
await new Promise((r) => setTimeout(r, 500)); // Can't use DFL sleep here.
|
||||
// deliberate partial import
|
||||
const DFLWebpack = await import('@decky/ui/dist/webpack');
|
||||
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
|
||||
|
||||
@@ -421,7 +421,7 @@ class PluginLoader extends Logger {
|
||||
try {
|
||||
switch (loadType) {
|
||||
case PluginLoadType.ESMODULE_V1:
|
||||
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js`);
|
||||
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
|
||||
let plugin = plugin_exports.default();
|
||||
|
||||
this.plugins.push({
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Adapted from a script provided by Jaynator495.
|
||||
# Make sure to place in home directory, chmod +x plugin-info.sh and then run with ./plugin-info.sh
|
||||
# Define the directory to scan
|
||||
directory_to_scan="~/homebrew/plugins"
|
||||
|
||||
# Loop through each subdirectory (one level deep)
|
||||
for dir in "$directory_to_scan"/*/; do
|
||||
# Check if package.json exists in the subdirectory
|
||||
if [ -f "${dir}package.json" ]; then
|
||||
# Extract name and version from the package.json file using jq
|
||||
name=$(jq -r '.name' "${dir}package.json")
|
||||
version=$(jq -r '.version' "${dir}package.json")
|
||||
|
||||
# Output the name and version
|
||||
echo "Directory: ${dir}"
|
||||
echo "Package Name: $name"
|
||||
echo "Version: $version"
|
||||
echo "-----------------------------"
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user