diff --git a/backend/decky_loader/localplatform/localplatform.py b/backend/decky_loader/localplatform/localplatform.py index 028eff8fc..c7085cd1d 100644 --- a/backend/decky_loader/localplatform/localplatform.py +++ b/backend/decky_loader/localplatform/localplatform.py @@ -37,6 +37,9 @@ def get_live_reload() -> bool: def get_keep_systemd_service() -> bool: return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1" +def get_use_cef_close_workaround() -> bool: + return ON_LINUX and os.getenv("USE_CEF_CLOSE_WORKAROUND", "1") == "1" + def get_log_level() -> int: return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[ os.getenv("LOG_LEVEL", "INFO") diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py index f22cb465d..45086aef1 100644 --- a/backend/decky_loader/localplatform/localplatformlinux.py +++ b/backend/decky_loader/localplatform/localplatformlinux.py @@ -1,3 +1,5 @@ +from re import compile +from asyncio import Lock import os, pwd, grp, sys, logging from subprocess import call, run, DEVNULL, PIPE, STDOUT from ..enums import UserType @@ -227,3 +229,39 @@ def get_unprivileged_user() -> str: user = 'deck' return user + +# Works around the CEF debugger TCP socket not closing properly when Steam restarts +# Group 1 is PID, group 2 is FD. this also filters for "steamwebhelper" in the process name. +cef_socket_lsof_regex = compile(r"^p(\d+)(?:\s|.)+csteamwebhelper(?:\s|.)+f(\d+)(?:\s|.)+TST=LISTEN") +close_cef_socket_lock = Lock() + +async def close_cef_socket(): + async with close_cef_socket_lock: + if _get_effective_user_id() != 0: + logger.warn("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: + 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) + + if not lsof_data: + logger.error("lsof regex match failed in close_cef_socket!") + return + + pid = lsof_data.group(1) + fd = lsof_data.group(2) + + 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})"]) + + if gdb_ret.returncode != 0: + logger.error(f"Failed to close CEF socket with gdb! return code: {str(gdb_ret.returncode)}", exc_info=True) + return + + logger.info("CEF socket closed") diff --git a/backend/decky_loader/localplatform/localplatformwin.py b/backend/decky_loader/localplatform/localplatformwin.py index 0724b59e2..52ade07c6 100644 --- a/backend/decky_loader/localplatform/localplatformwin.py +++ b/backend/decky_loader/localplatform/localplatformwin.py @@ -55,4 +55,7 @@ def get_unprivileged_user() -> str: return os.getenv("UNPRIVILEGED_USER", os.getlogin()) async def restart_webhelper() -> bool: - return True # Stubbed \ No newline at end of file + return True # Stubbed + +async def close_cef_socket(): + return # Stubbed \ No newline at end of file diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py index 4850cdef1..17226ebcb 100644 --- a/backend/decky_loader/utilities.py +++ b/backend/decky_loader/utilities.py @@ -20,9 +20,8 @@ if TYPE_CHECKING: from .main import PluginManager from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab -from .localplatform.localplatform import ON_WINDOWS from . import helpers -from .localplatform.localplatform import service_stop, service_start, get_home_path, get_username +from .localplatform.localplatform import ON_WINDOWS, service_stop, service_start, get_home_path, get_username, get_use_cef_close_workaround, close_cef_socket class FilePickerObj(TypedDict): file: Path @@ -78,6 +77,7 @@ def __init__(self, context: PluginManager) -> None: context.ws.add_route("utilities/get_tab_id", self.get_tab_id) context.ws.add_route("utilities/get_user_info", self.get_user_info) context.ws.add_route("utilities/http_request", self.http_request_legacy) + context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) context.web_app.add_routes([ @@ -287,6 +287,10 @@ async def stop_ssh(self): await service_stop(helpers.SSHD_UNIT) return True + async def close_cef_socket(self): + if get_use_cef_close_workaround(): + await close_cef_socket() + async def filepicker_ls(self, path: str | None = None, include_files: bool = True, diff --git a/frontend/src/steamfixes/index.ts b/frontend/src/steamfixes/index.ts index e3f2b2848..f0e04c4d7 100644 --- a/frontend/src/steamfixes/index.ts +++ b/frontend/src/steamfixes/index.ts @@ -1,5 +1,6 @@ -// import reloadFix from './reload'; -import restartFix from './restart'; +// import restartFix from './restart'; +import cefSocketFix from "./socket"; + let fixes: Function[] = []; export function deinitSteamFixes() { @@ -7,6 +8,6 @@ export function deinitSteamFixes() { } export async function initSteamFixes() { - // fixes.push(await reloadFix()); - fixes.push(await restartFix()); + fixes.push(cefSocketFix()); + // fixes.push(await restartFix()); } diff --git a/frontend/src/steamfixes/socket.ts b/frontend/src/steamfixes/socket.ts new file mode 100644 index 000000000..a003b417c --- /dev/null +++ b/frontend/src/steamfixes/socket.ts @@ -0,0 +1,16 @@ +import Logger from "../logger"; + +const logger = new Logger('CEFSocketFix'); + +const closeCEFSocket = DeckyBackend.callable<[], void>("utilities/close_cef_socket"); + +export default function cefSocketFix() { + const reg = window.SteamClient?.User?.RegisterForShutdownStart(async () => { + logger.log("Closing CEF socket before shutdown"); + await closeCEFSocket(); + }); + + if (reg) logger.debug("CEF shutdown handler ready"); + + return () => reg?.unregister(); +} \ No newline at end of file