From 195143664bf04d4e555de491b2d395fdfd41ee82 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 3 Aug 2026 07:39:30 +0545 Subject: [PATCH] fix(recording): track backend subprocess for active state active shelled out to pidof on every read, blocking the main loop and missing our own recorder handle. Track the spawned Gio.Subprocess for an exact answer and cache the external pidof probe for 2s. Resets the cache at ownership boundaries (start/stop). --- src/services/screenrecorder.py | 31 ++++++++++++++++++++++++++++--- src/shared/capture.py | 15 +++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/services/screenrecorder.py b/src/services/screenrecorder.py index 8e33bb9..8b0861a 100644 --- a/src/services/screenrecorder.py +++ b/src/services/screenrecorder.py @@ -24,6 +24,12 @@ from utils.functions import is_app_running, resolve_monitor _RECORDER_PROCESSES = ("wf-recorder", "gpu-screen-recorder") +# How long a cached external-recorder probe stays valid. Detecting a recorder +# spawned outside this service requires a ``pidof`` call; caching the result +# means reading the ``active`` property never spawns a subprocess on the main +# loop more than once per this window. +_ACTIVE_PROBE_TTL = 2.0 + class ScreenRecorder(Service): """Record the screen through a pluggable :class:`CaptureBackend`.""" @@ -69,6 +75,8 @@ class ScreenRecorder(Service): self._output_file: str | None = None self._start_time: float | None = None self._tick_source_id: int | None = None + self._external_recorder_active = False + self._external_probe_time = 0.0 self.recordings_dir = Path.home() / "Videos" / "Recordings" self.recordings_dir.mkdir(parents=True, exist_ok=True) @@ -121,10 +129,21 @@ class ScreenRecorder(Service): def active(self) -> bool: """Whether a recorder process is actually running. - Unlike :attr:`recording`, this reflects the real process state and - also detects recordings started outside this service. + A recording started through this service is tracked exactly via its + subprocess handle. Recordings started outside modus are detected with + a short-lived cached ``pidof`` probe, so reading this property never + blocks the main loop more than once per ``_ACTIVE_PROBE_TTL`` seconds. """ - return any(is_app_running(proc) for proc in _RECORDER_PROCESSES) + ours = self._backend.is_recorder_running() + if ours is not None: + return ours + now = time.monotonic() + if now - self._external_probe_time >= _ACTIVE_PROBE_TTL: + self._external_recorder_active = any( + is_app_running(proc) for proc in _RECORDER_PROCESSES + ) + self._external_probe_time = now + return self._external_recorder_active # -- public API -------------------------------------------------------- @@ -220,6 +239,10 @@ class ScreenRecorder(Service): self._recording = True self._paused = False self._start_time = time.monotonic() + # This service now owns the recorder: drop the external cache so the + # next ``active`` read resolves exactly from the tracked subprocess. + self._external_recorder_active = False + self._external_probe_time = 0.0 self._start_tick() self.notify("output-file") self.notify("recording") @@ -230,6 +253,8 @@ class ScreenRecorder(Service): self._recording = False self._paused = False self._start_time = None + # Force a fresh external probe on the next ``active`` read. + self._external_probe_time = 0.0 self.notify("recording") self.notify("busy") self.notify("elapsed-seconds") diff --git a/src/shared/capture.py b/src/shared/capture.py index 0d7e50e..9e5d759 100644 --- a/src/shared/capture.py +++ b/src/shared/capture.py @@ -147,6 +147,7 @@ class HyprlandCaptureBackend(CaptureBackend): tool: find_binary(tool) is not None for tool in ("hyprshot", "grim", "slurp", self._RECORDER) } + self._recorder_proc: Gio.Subprocess | None = None @property def capabilities(self) -> BackendCapabilities: @@ -163,6 +164,18 @@ class HyprlandCaptureBackend(CaptureBackend): """Return whether a wrapped tool is available on this system.""" return self._tools.get(tool, False) + def is_recorder_running(self) -> bool | None: + """Return whether the recorder spawned by this backend is still alive. + + ``None`` means no recorder was started through this backend, so the + caller must fall back to external detection. Cheap: reads the already + tracked subprocess instead of shelling out. + """ + proc = self._recorder_proc + if proc is None: + return None + return not proc.get_if_exited() + # screenshots def screenshot(self, request: ScreenshotRequest, on_result: ResultCallback) -> bool: @@ -322,10 +335,12 @@ class HyprlandCaptureBackend(CaptureBackend): if proc is None: on_result(CaptureResult.failure(f"failed to launch {self._RECORDER}")) return False + self._recorder_proc = proc on_result(CaptureResult.ok(str(request.output_file))) return True def stop_recording(self) -> bool: + self._recorder_proc = None kill_process(self._RECORDER) return True