From 61154dbda3658a849fc604515c51b96611b9db16 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 13 Jul 2026 04:45:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(plugin):=20safe=20recording=20start=20?= =?UTF-8?q?=E2=80=94=20no=20mid-game=20config=20rewrite,=20scoped=20log=20?= =?UTF-8?q?clear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/decky-vault/main.py | 52 ++++++++++-- plugins/decky-vault/src/index.tsx | 21 +++-- plugins/decky-vault/src/lib/api.ts | 12 +++ plugins/decky-vault/src/lib/store.ts | 8 ++ plugins/decky-vault/tests/test_clear_log.py | 87 +++++++++++++++++++++ 5 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 plugins/decky-vault/tests/test_clear_log.py diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 00ea939..4178f88 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -367,20 +367,56 @@ exec mangohud "$@" return {"error": f"Failed to read log: {str(e)}"} async def clear_mangohud_log(self) -> dict: - """RPC: Delete all MangoHud log files in /tmp/ so the next recording starts fresh.""" + """RPC: Delete stale MangoHud log files in /tmp/ so the next recording + starts fresh. Only touches files whose name contains 'MangoHud'; never + bare /tmp/*.log or /tmp/*.csv. Skips files modified in the last 3s + (an active session may still have them open).""" import glob + import time + RECENT_WINDOW_S = 3 + now = time.time() + deleted, skipped = [], [] try: - for pattern in ["/tmp/*MangoHud*", "/tmp/*.csv", "/tmp/*.log"]: + for pattern in ["/tmp/*MangoHud*"]: for f in glob.glob(pattern): - if os.path.isfile(f): - try: - os.remove(f) - except (IOError, PermissionError): - pass - return {"success": True} + if not os.path.isfile(f): + continue + try: + if now - os.path.getmtime(f) < RECENT_WINDOW_S: + skipped.append({"name": os.path.basename(f), "reason": "active"}) + continue + os.remove(f) + deleted.append({"name": os.path.basename(f)}) + except (IOError, PermissionError): + skipped.append({"name": os.path.basename(f), "reason": "perm"}) + return {"success": True, "deleted": deleted, "skipped": skipped} + except Exception as e: + return {"success": False, "error": str(e), "deleted": deleted, "skipped": skipped} + + async def delete_log_file(self, path: str) -> dict: + """RPC: Delete a single, specific MangoHud log file. The path must be + under /tmp and its basename must contain 'MangoHud'. Defence in depth + so a bad/stale path can never delete unrelated files.""" + try: + if not path: + return {"success": False, "error": "No path provided"} + abs_path = os.path.abspath(path) + if not abs_path.startswith("/tmp/"): + return {"success": False, "error": "Refusing to delete file outside /tmp"} + if "MangoHud" not in os.path.basename(abs_path): + return {"success": False, "error": "Refusing to delete non-MangoHud file"} + if not os.path.exists(abs_path): + return {"success": True, "deleted": False, "note": "already gone"} + os.remove(abs_path) + return {"success": True, "deleted": True, "path": abs_path} except Exception as e: return {"success": False, "error": str(e)} + async def find_mangohud_log(self) -> dict: + """RPC: Return the path of the most recent MangoHud log in /tmp/, or null.""" + path = await self._find_mangohud_log() + return {"path": path} + async def get_hardware_info(self) -> dict: """RPC: Detect hardware model from DMI. Returns {slug, name, raw}.""" # Steam Deck models: Jupiter = LCD, Galileo = OLED diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index c1273f1..cba24af 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -12,7 +12,8 @@ import { useSettings, useSession, useGameDetection } from "./lib/store" import { readAndParseMangohudLog, clearMangohudLog, - writeMangohudConfig, + deleteLogFile, + findMangohudLog, startMangohudLogging, stopMangohudLogging, getHardwareInfo, @@ -37,6 +38,7 @@ function Content() { onGameStart, onGameStop, setGameName, + setLastLogPath, } = useSession() // ── Game detection via polling ──────────────────────────────── @@ -44,10 +46,13 @@ function Content() { // ── Handle start recording ──────────────────────────────────── async function handleStart() { - // Write MangoHud config with logging settings - await writeMangohudConfig() - // Clear any previous log file - await clearMangohudLog() + // Clear the previous session's specific log if we know it; else safe-clear. + const prev = session.lastLogPath + if (prev) { + await deleteLogFile(prev) + } else { + await clearMangohudLog() + } // Fire-and-forget: try to start MangoHud logging (retries until game launches) startMangohudLogging() startRecording() @@ -60,12 +65,14 @@ function Content() { await stopMangohudLogging() stopRecording() - // Parse the MangoHud log - const logResult = await readAndParseMangohudLog() + // Find the most recent MangoHud log, parse it, remember its path + const logPath = await findMangohudLog() + const logResult = await readAndParseMangohudLog(logPath.path ?? undefined) if (logResult.error) { setError(logResult.error) return } + setLastLogPath(logPath.path ?? null) // Read system info in parallel const [hwInfo, osVersion] = await Promise.all([ diff --git a/plugins/decky-vault/src/lib/api.ts b/plugins/decky-vault/src/lib/api.ts index 4881a5f..25ff508 100644 --- a/plugins/decky-vault/src/lib/api.ts +++ b/plugins/decky-vault/src/lib/api.ts @@ -36,9 +36,21 @@ export const readAndParseMangohudLog = callable<[logPath?: string], { export const clearMangohudLog = callable<[], { success: boolean + deleted?: Array<{ name: string }> + skipped?: Array<{ name: string; reason: string }> error?: string }>("clear_mangohud_log") +export const deleteLogFile = callable<[path: string], { + success: boolean + deleted?: boolean + error?: string +}>("delete_log_file") + +export const findMangohudLog = callable<[], { + path: string | null +}>("find_mangohud_log") + export const startMangohudLogging = callable<[], { success: boolean error?: string diff --git a/plugins/decky-vault/src/lib/store.ts b/plugins/decky-vault/src/lib/store.ts index e4be2d2..185b52e 100644 --- a/plugins/decky-vault/src/lib/store.ts +++ b/plugins/decky-vault/src/lib/store.ts @@ -36,6 +36,8 @@ export interface SessionData { protonVersion: string versionString: string buildId: string + // Path of the MangoHud log captured for this session (for targeted cleanup on restart) + lastLogPath: string | null // Manual inputs (filled by user in the form) upscalerType: string upscalerVersion: string @@ -70,6 +72,7 @@ function createEmptySession(): SessionData { protonVersion: "", versionString: "", buildId: "", + lastLogPath: null, upscalerType: "none", upscalerVersion: "", frameGenMethod: "none", @@ -182,6 +185,10 @@ export function useSession() { setSession((prev) => ({ ...prev, gameName: name, appId: appId ?? prev.appId })) }, []) + const setLastLogPath = useCallback((p: string | null) => { + setSession((prev) => ({ ...prev, lastLogPath: p })) + }, []) + return { recordingState, session, @@ -196,6 +203,7 @@ export function useSession() { onGameStart, onGameStop, setGameName, + setLastLogPath, } } diff --git a/plugins/decky-vault/tests/test_clear_log.py b/plugins/decky-vault/tests/test_clear_log.py new file mode 100644 index 0000000..30ea80a --- /dev/null +++ b/plugins/decky-vault/tests/test_clear_log.py @@ -0,0 +1,87 @@ +"""Tests for safe MangoHud log clearing (Bug 1 fix).""" +import os +import tempfile +import time +import pytest + + +def _touch(path, mtime_age=10): + """Create a file at path, optionally backdated mtime by mtime_age seconds.""" + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + f.write("data") + if mtime_age > 0: + t = time.time() - mtime_age + os.utime(path, (t, t)) + + +def _safe_clear_mangohud_logs(tmpdir, now=None, recent_window_s=3): + """Mirror of Plugin.clear_mangohud_log safe logic, operating on tmpdir.""" + import glob + if now is None: + now = time.time() + deleted, skipped = [], [] + patterns = [ + os.path.join(tmpdir, "*MangoHud*"), + ] + for pat in patterns: + for f in glob.glob(pat): + if not os.path.isfile(f): + continue + try: + if now - os.path.getmtime(f) < recent_window_s: + skipped.append({"name": os.path.basename(f), "reason": "active"}) + continue + os.remove(f) + deleted.append({"name": os.path.basename(f)}) + except (IOError, PermissionError): + skipped.append({"name": os.path.basename(f), "reason": "perm"}) + return {"success": True, "deleted": deleted, "skipped": skipped} + + +def test_only_mangohud_files_removed(): + with tempfile.TemporaryDirectory() as tmp: + _touch(os.path.join(tmp, "MangoHud-1.csv"), mtime_age=10) + _touch(os.path.join(tmp, "system.log"), mtime_age=10) # MUST be untouched + _touch(os.path.join(tmp, "other.csv"), mtime_age=10) # MUST be untouched + res = _safe_clear_mangohud_logs(tmp) + assert res["success"] is True + names = [d["name"] for d in res["deleted"]] + assert "MangoHud-1.csv" in names + assert "system.log" not in names and "other.csv" not in names + assert os.path.exists(os.path.join(tmp, "system.log")) + assert os.path.exists(os.path.join(tmp, "other.csv")) + + +def test_active_recent_file_skipped(): + with tempfile.TemporaryDirectory() as tmp: + _touch(os.path.join(tmp, "MangoHud-active.csv"), mtime_age=0) + res = _safe_clear_mangohud_logs(tmp) + assert res["deleted"] == [] + assert any(s["name"] == "MangoHud-active.csv" for s in res["skipped"]) + assert os.path.exists(os.path.join(tmp, "MangoHud-active.csv")) + + +def _validate_log_path(path, tmpdir): + """Mirror of Plugin.delete_log_file path validation.""" + if not path: + return False + abs_path = os.path.abspath(path) + if not abs_path.startswith(os.path.abspath(tmpdir) + os.sep): + return False + base = os.path.basename(abs_path) + if "MangoHud" not in base: + return False + return True + + +def test_delete_log_file_rejects_outside_tmp(): + with tempfile.TemporaryDirectory() as tmp: + assert _validate_log_path("/etc/passwd", tmp) is False + assert _validate_log_path(os.path.expanduser("~/x.log"), tmp) is False + + +def test_delete_log_file_rejects_non_mangohud(): + with tempfile.TemporaryDirectory() as tmp: + assert _validate_log_path(os.path.join(tmp, "system.log"), tmp) is False + assert _validate_log_path(os.path.join(tmp, "MangoHud-1.csv"), tmp) is True \ No newline at end of file