Compare commits

..

27 Commits

Author SHA1 Message Date
AAGaming 1781c19c11 Fix broken checkboxes on Beta Steam client (#710) 2024-10-04 13:26:00 -04:00
Lukas Senionis 1ef3cb8307 fix(http_request): remove conflicting CORS headers (#708) 2024-10-04 13:15:58 -04:00
WerWolvTranslationBot 5bc4dc684d Translations update from Weblate (#707)
Co-authored-by: ayssia <nynaevealmearah@gmail.com>
2024-10-03 17:28:00 -04:00
AAGaming 4cff530b52 Fix missing components on Oct 2 2024 Steam Beta (#709) 2024-10-03 17:08:35 -04:00
Marco Rodolfi 2f90a4fcf7 Rebase semver parsing on main (#677)
Co-authored-by: Marco Rodolfi <marco.rodolfi.1992@gmail.com>
2024-09-17 06:21:31 -07:00
AAGaming 24fce1e093 bump @decky/ui to include findSP fix 2024-09-16 19:31:04 -04:00
AAGaming 97b12972ee shut ts up 2024-09-16 16:21:00 -04:00
AAGaming f69eb72df9 wait for SteamApp init stage 1 to finish before loading decky's frontend bundle
should fix the startup race condition
2024-09-16 16:17:20 -04:00
WerWolvTranslationBot 24215c0732 Translations update from Weblate (#704)
* Translated using Weblate (German)

Currently translated at 100.0% (158 of 158 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/de/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (158 of 158 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/nl/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (158 of 158 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/bg/

---------

Co-authored-by: SecularSteve <fairfull.playing@gmail.com>
2024-09-14 23:01:47 -07:00
Sims e87ce625fb Test (#701) 2024-09-13 16:59:35 -07:00
AAGaming 1284075d02 update release systemd service 2024-09-11 22:54:16 -04:00
TrainDoctor d2c5aef58b Update release.yml 2024-09-11 19:49:39 -07:00
AAGaming caac379b08 just sleep 500ms for now to work around startup race condition 2024-09-11 21:50:48 -04:00
AAGaming c487a6e15a deprecate install scripts in repo (use decky-installer/cli instead, they're the same scripts but more up to date) 2024-09-11 21:02:41 -04:00
AAGaming 9df5f00068 drop TimeoutStopSec to 15s 2024-09-11 21:00:19 -04:00
AAGaming 508408ad5a use signals to shut down plugins instead of sending a socket message
should reduce or outright prevent shutdown stalls
2024-09-11 20:35:24 -04:00
AAGaming ef4ca204bd potentially fix startup race condition 2024-09-11 20:16:49 -04:00
AAGaming 1d7af36a2b add cache bust param to index.js of esmodule plugins
should fix hot reload
2024-09-11 19:38:58 -04:00
TrainDoctor 6b78e0ae9c Update bug_report.yml 2024-09-05 12:21:58 -07:00
TrainDoctor 1f5d5f9f1a Update bug_report.yml 2024-09-05 12:20:32 -07:00
TrainDoctor f7a47127a7 Update bug_report.yml 2024-09-05 12:19:12 -07:00
TrainDoctor 494f8dac5e Update bug_report.yml
Add new field requiring all installed plugins to be listed.
2024-09-05 12:14:33 -07:00
TrainDoctor bcc14848c5 Create plugin-info.sh
Add plugin-info script for debugging, thanks @Jaynator495!
2024-09-05 12:09:39 -07:00
AAGaming 0e40374b10 This also shouldn't have applied to stabls 2024-09-04 08:45:54 -04:00
AAGaming 81ffe11106 This shouldn't have applied to stable 2024-09-04 08:45:26 -04:00
AAGaming d06494885a fix external links softlocking the ui in testing store cta 2024-09-01 20:40:12 -04:00
Sims a6e4bcf052 Fix updater taking a long time (#696)
Replaces subprocess with asyncio.subprocess in some localplatformlinux functions and improves shutdown handling
Co-authored-by: AAGaming <aagaming@riseup.net>
2024-09-01 19:45:47 -04:00
27 changed files with 289 additions and 235 deletions
+19 -3
View File
@@ -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
+2 -2
View File
@@ -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
+33 -2
View File
@@ -99,12 +99,14 @@
}
},
"PluginListIndex": {
"freeze": "Замразяване на актуализациите",
"hide": "Бърз достъп: Скриване",
"no_plugin": "Няма инсталирани добавки!",
"plugin_actions": "Действия с добавката",
"reinstall": "Преинсталиране",
"reload": "Презареждане",
"show": "Бърз достъп: Показване",
"unfreeze": "Разрешаване на актуализациите",
"uninstall": "Деинсталиране",
"update_all_one": "Обновяване на 1 добавка",
"update_all_other": "Обновяване на {{count}} добавки",
@@ -192,9 +194,19 @@
"SettingsIndex": {
"developer_title": "Разработчик",
"general_title": "Общи",
"plugins_title": "Добавки"
"plugins_title": "Добавки",
"testing_title": "Тестване"
},
"Store": {
"download_progress_info": {
"download_zip": "Изтегляне на плъгина",
"increment_count": "Увеличаване на броя изтегляния",
"installing_plugin": "Инсталиране на плъгина",
"open_zip": "Отваряне на zip файла",
"parse_zip": "Разглеждане на zip файла",
"start": "Иницииране",
"uninstalling_previous": "Деинсталиране на предишното копие"
},
"store_contrib": {
"desc": "Ако искате да допринесете към магазина за добавки на Decky, разгледайте хранилището SteamDeckHomebrew/decky-plugin-template в GitHub. Може да намерите информация относно разработката и разпространението във файла README.",
"label": "Допринасяне"
@@ -218,9 +230,17 @@
"about": "Относно",
"alph_asce": "По азбучен ред (Я -> А)",
"alph_desc": "По азбучен ред (А -> Я)",
"date_asce": "Най-старият първи",
"date_desc": "Най-новият първи",
"downloads_asce": "Най-малко изтеглени първи",
"downloads_desc": "Най-изтеглени първи",
"title": "Разглеждане"
},
"store_testing_cta": "Помислете дали искате да тествате новите добавки, за да помогнете на екипа на Decky Loader!"
"store_testing_cta": "Помислете дали искате да тествате новите добавки, за да помогнете на екипа на Decky Loader!",
"store_testing_warning": {
"desc": "Можете да използвате този канал на магазина, за да тествате най-новите версии на плъгините. Не забравяйте да оставите обратна връзка в GitHub, за да може плъгинът да бъде актуализиран за всички потребители.",
"label": "Добре дошли в канала на магазина за тестване"
}
},
"StoreSelect": {
"custom_store": {
@@ -234,6 +254,17 @@
"testing": "Тестване"
}
},
"Testing": {
"download": "Изтегляне",
"error": "Грешка при инсталирането на PR",
"header": "Следните версии на Decky Loader са създадени от отворени заявки за изтегляне от трети страни. Екипът на Decky Loader не е проверявал тяхната функционалност или сигурност и е възможно те да са остарели.",
"loading": "Зареждане на отворени заявки за изтегляне...",
"start_download_toast": "Изтегляне на PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "Отворете Decky Store",
"settings_desc": "Отворете настройките на Decky"
},
"Updater": {
"decky_updates": "Обновления на Decky",
"no_patch_notes_desc": "няма бележки за промените в тази версия",
+14 -1
View File
@@ -198,6 +198,15 @@
"testing_title": "Testen"
},
"Store": {
"download_progress_info": {
"download_zip": "Plugin herunterladen",
"increment_count": "Erhöhen der Downloadanzahl",
"installing_plugin": "Plugin installieren",
"open_zip": "Öffnen der Zip-Datei",
"parse_zip": "Parsen der Zip-Datei",
"start": "Initialisieren",
"uninstalling_previous": "Vorherige Kopie deinstallieren"
},
"store_contrib": {
"desc": "Wenn du Erweiterungen im Decky Store veröffentlichen willst, besuche die SteamDeckHomebrew/decky-plugin-template Repository auf GitHub. Informationen rund um Entwicklung und Veröffentlichung findest du in der README.",
"label": "Mitwirken"
@@ -246,7 +255,11 @@
}
},
"Testing": {
"download": "Download"
"download": "Download",
"error": "Fehler beim Installieren von PR",
"header": "Die folgenden Versionen von Decky Loader wurden aus offenen Pull Requests von Dritten erstellt. Das Decky Loader-Team hat ihre Funktionalität oder Sicherheit nicht überprüft, und sie können veraltet sein.",
"loading": "Offene Pull Requests laden...",
"start_download_toast": "Herunterladen von PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "Decky Store Öffnen",
+19 -6
View File
@@ -2,9 +2,9 @@
"BranchSelect": {
"update_channel": {
"label": "Updatekanaal",
"prerelease": "Prerelease",
"prerelease": "Vooruitgave",
"stable": "Stabiel",
"testing": "Testing"
"testing": "Testen"
}
},
"Developer": {
@@ -188,7 +188,7 @@
"header": "Overige"
},
"updates": {
"header": "Updates"
"header": "Bijwerkingen"
}
},
"SettingsIndex": {
@@ -198,6 +198,15 @@
"testing_title": "Testen"
},
"Store": {
"download_progress_info": {
"download_zip": "Plugin downloaden",
"increment_count": "Aantal downloads verhogen",
"installing_plugin": "Plugin installeren",
"open_zip": "Zip-bestand openen",
"parse_zip": "Zip-bestand parseren",
"start": "Initialiseren",
"uninstalling_previous": "Vorige kopie verwijderen"
},
"store_contrib": {
"desc": "Als je wilt bijdragen aan de Decky Plugin Store, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
"label": "Bijdragen"
@@ -242,11 +251,15 @@
"custom": "Aangepast",
"default": "Standaard",
"label": "Winkelkanaal",
"testing": "Testing"
"testing": "Testen"
}
},
"Testing": {
"download": "Downloaden"
"download": "Downloaden",
"error": "Fout bij installatie van PR",
"header": "De volgende versies van Decky Loader zijn gebouwd op basis van open Pull Requests van derden. Het Decky Loader-team heeft hun functionaliteit of veiligheid niet gecontroleerd en ze kunnen verouderd zijn.",
"loading": "Openstaande Pull Requests laden...",
"start_download_toast": "PR #{{id}} downloaden"
},
"TitleView": {
"decky_store_desc": "Decky Store openen",
@@ -261,7 +274,7 @@
"checking": "Bezig met controleren op updates",
"cur_version": "Huidige versie: {{ver}}",
"install_button": "Bijwerken",
"label": "Updates",
"label": "Bijwerkingen",
"lat_version": "Bijwerkt: versie {{ver}}",
"reloading": "Bezig met herstarten",
"updating": "Bezig met bijwerken"
+14 -1
View File
@@ -191,6 +191,15 @@
"testing_title": "测试"
},
"Store": {
"download_progress_info": {
"download_zip": "正在下载插件",
"increment_count": "正在计入下载次数",
"installing_plugin": "正在安装插件",
"open_zip": "正在打开 ZIP 文件",
"parse_zip": "正在解析 ZIP 文件",
"start": "正在初始化",
"uninstalling_previous": "正在卸载之前的版本"
},
"store_contrib": {
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库。有关开发和分发插件的信息,请查看 README 文件。",
"label": "贡献"
@@ -239,7 +248,11 @@
}
},
"Testing": {
"download": "下载"
"download": "下载",
"error": "安装 PR 时出错",
"header": "以下版本的 Decky Loader 是根据开放的第三方 Pull Request 构建的。Decky Loader 团队尚未验证这些版本的功能或安全性,且它们可能已经过期。",
"loading": "正在加载尚未合并的 Pull Request ...",
"start_download_toast": "正在下载 PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "打开 Decky 商店",
@@ -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)
+4 -3
View File
@@ -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)
+15 -12
View File
@@ -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:
+27 -12
View File
@@ -4,13 +4,14 @@ 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
from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
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]
@@ -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,9 +51,16 @@ 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
if ON_LINUX:
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)
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
# export a bunch of environment variables to help plugin developers
@@ -155,22 +165,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:
+5 -2
View File
@@ -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")
+18 -4
View File
@@ -9,7 +9,7 @@ from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession
from aiohttp import ClientSession, hdrs
from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
@@ -153,14 +153,14 @@ class Utilities:
headers["User-Agent"] = helpers.user_agent
for excluded_header in excluded_default_headers:
self.logger.debug(f"Excluding default header {excluded_header}")
if excluded_header in headers:
self.logger.debug(f"Excluding default header {excluded_header}: {headers[excluded_header]}")
del headers[excluded_header]
if "X-Decky-Fetch-Excluded-Headers" in req.headers:
for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
self.logger.debug(f"Excluding header {excluded_header}")
if excluded_header in headers:
self.logger.debug(f"Excluding header {excluded_header}: {headers[excluded_header]}")
del headers[excluded_header]
for header in req.headers:
@@ -187,7 +187,21 @@ class Utilities:
# defeat the point of this proxy.
async with ClientSession(auto_decompress=False) as web:
async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
res = StreamResponse(headers=web_res.headers, status=web_res.status)
# Whenever the aiohttp_cors is used, it expects a near complete control over whatever headers are needed
# for `aiohttp_cors.ResourceOptions`. As a server, if you delegate CORS handling to aiohttp_cors,
# the headers below must NOT be set. Otherwise they would be overwritten by aiohttp_cors and there would be
# logic bugs, so it was probably a smart choice to assert if the headers are present.
#
# However, this request handler method does not act like our own local server, it always acts like a proxy
# where we do not have control over the response headers. For responses that do not allow CORS, we add the support
# via aiohttp_cors. For responses that allow CORS, we have to remove the conflicting headers to allow
# aiohttp_cors handle it for us as if there was no CORS support.
aiohttp_cors_compatible_headers = web_res.headers.copy()
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_ALLOW_ORIGIN, default=None)
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS, default=None)
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_EXPOSE_HEADERS, default=None)
res = StreamResponse(headers=aiohttp_cors_compatible_headers, status=web_res.status)
if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
res.enable_chunked_encoding()
+2 -69
View File
@@ -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
+2 -69
View File
@@ -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
View File
@@ -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}
+1 -1
View File
@@ -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}
+2 -1
View File
@@ -47,7 +47,8 @@
}
},
"dependencies": {
"@decky/ui": "^4.7.1",
"@decky/ui": "^4.7.4",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2",
+13 -5
View File
@@ -9,8 +9,11 @@ importers:
.:
dependencies:
'@decky/ui':
specifier: ^4.7.1
version: 4.7.1
specifier: ^4.7.4
version: 4.7.4
compare-versions:
specifier: ^6.1.1
version: 6.1.1
filesize:
specifier: ^10.1.2
version: 10.1.2
@@ -215,8 +218,8 @@ packages:
'@decky/api@1.1.1':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
'@decky/ui@4.7.1':
resolution: {integrity: sha512-yJwBgW+J2cMDfMkmcDFtzsubhUjekFZAtCnP55QEJ/1UKGR7sLNOvDLFYi1h5PI0K4L1XYcAMKHwbYFFTzcDTA==}
'@decky/ui@4.7.4':
resolution: {integrity: sha512-ziCP3akLJVYG5FFoS0ao9MYEYJ09g44FP4xMmOUFe8SFRjC9BqBvbJBF0+OUOKgp2C7SJ0rNPSIFm4RwCUvoug==}
'@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -848,6 +851,9 @@ packages:
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -2289,7 +2295,7 @@ snapshots:
'@decky/api@1.1.1': {}
'@decky/ui@4.7.1': {}
'@decky/ui@4.7.4': {}
'@esbuild/aix-ppc64@0.20.2':
optional: true
@@ -2816,6 +2822,8 @@ snapshots:
commondir@1.0.1: {}
compare-versions@6.1.1: {}
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
+16
View File
@@ -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;
+3 -2
View File
@@ -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>
+5 -4
View File
@@ -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>
+8 -15
View File
@@ -5,23 +5,16 @@ interface Window {
}
(async () => {
// Wait for main webpack chunks to definitely be loaded
console.time('[Decky:Boot] Waiting for main Webpack chunks...');
while (!window.webpackChunksteamui || window.webpackChunksteamui.length < 5) {
await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here.
}
console.timeEnd('[Decky:Boot] Waiting for main Webpack chunks...');
console.debug('[Decky:Boot] Frontend init');
// Wait for the React root to be mounted
console.time('[Decky:Boot] Waiting for React root mount...');
let root;
while (
!(root = document.getElementById('root')) ||
!(root as any)[Object.keys(root).find((k) => k.startsWith('__reactContainer$')) as string]
) {
await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here.
console.time('[Decky:Boot] Waiting for SteamApp init stage 1 to finish...');
// @ts-expect-error TODO type BFinishedInitStageOne in @decky/ui
while (!window.App?.BFinishedInitStageOne()) {
await new Promise((r) => setTimeout(r, 0)); // Can't use DFL sleep here.
}
console.timeEnd('[Decky:Boot] Waiting for React root mount...');
console.timeEnd('[Decky:Boot] Waiting for SteamApp init stage 1 to finish...');
if (!window.SP_REACT) {
console.debug('[Decky:Boot] Setting up Webpack & React globals...');
+4 -3
View File
@@ -168,8 +168,9 @@ class PluginLoader extends Logger {
Promise.all([this.getUserInfo(), this.updateVersion()])
.then(() => this.loadPlugins())
.then(() => this.checkPluginUpdates())
.then(() => this.log('Initialized'));
.then(() => this.log('Initialized'))
.then(() => sleep(30000)) // Internet might not immediately be up
.then(() => this.checkPluginUpdates());
}
private checkForSP(): boolean {
@@ -421,7 +422,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({
+11 -1
View File
@@ -1,3 +1,5 @@
import { compare } from 'compare-versions';
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin';
import { getSetting, setSetting } from './utils/settings';
@@ -137,7 +139,15 @@ export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUp
const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name);
if (remotePlugin && remotePlugin.versions?.length > 0 && plugin.version != remotePlugin?.versions?.[0]?.name) {
//FIXME: Ugly hack since plugin.version might be null during evaluation,
//so this will set the older version possible
const curVer = plugin.version ? plugin.version : '0.0';
if (
remotePlugin &&
remotePlugin.versions?.length > 0 &&
plugin.version != remotePlugin?.versions?.[0]?.name &&
compare(remotePlugin?.versions?.[0]?.name, curVer, '>')
) {
updateMap.set(plugin.name, remotePlugin.versions[0]);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ while :; do
if [[ $NEWTARGET != "" ]] && [[ $NEWTARGET != $TARGET ]]; then
echo found new tab at $NEWTARGET
TARGET=$NEWTARGET
TARGETURL="devtools://devtools/bundled/inspector.html?remoteFrontend=true&ws=$ADDR/devtools/page/$TARGET"
TARGETURL="http://$ADDR/devtools/inspector.html?ws=$ADDR/devtools/page/$TARGET"
LOCALTARGET=$(echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
{"id": 2, "method": "Target.closeTarget", "params": {"targetId": "'$LOCALTARGET'"}}' \
+21
View File
@@ -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