From c4647dda6b482b419d8b120f0a14b59b326acece Mon Sep 17 00:00:00 2001 From: ocpway Date: Thu, 30 Jul 2026 04:43:45 +0300 Subject: [PATCH] Fix hang on click and reconnect after daemon restart Two root causes shared one symptom: the waybar module froze after a click and needed a waybar restart to recover. 1. oc-waybar-ctl used socat's UNIX-SENDTO, a bidirectional relay that can block on a datagram socket. waybar runs the click command on its per-module event thread and stops updating the module until the command returns, so a hung socat froze the module. Replace the socat/nc chain with a bounded python3 sendto (2s timeout). python3 is guaranteed present since the daemon needs it. 2. The daemon ran oc raw json with a blocking subprocess.run on the main loop, so a slow network fetch (up to 60s) stalled the 5s rotation and queued clicks. Move fetch_ocd to a worker thread; the main loop adopts the result when it lands and never blocks. Rotation and click draining stay live during a fetch. Reconnect after a daemon restart now works because the click helper no longer wedges waybar's event thread; the module's interval-1 cat re-reads state.json as soon as the new daemon rewrites it (verified: state.json returns within 1s). --- scripts/ocd-waybar | 25 ++++++++++++++++++++++--- scripts/ocd-waybar-ctl | 33 ++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/scripts/ocd-waybar b/scripts/ocd-waybar index 26e33f3..73c8d94 100755 --- a/scripts/ocd-waybar +++ b/scripts/ocd-waybar @@ -20,6 +20,7 @@ import signal import socket import subprocess import sys +import threading import time from datetime import datetime, timezone from html import escape @@ -303,6 +304,11 @@ class Daemon: self.last_key = None self.sock = None self.running = True + # The ocd poll runs in a worker thread so the main loop never + # blocks on a slow network fetch. Rotation and clicks stay live. + self._fetch_thread = None + self._fresh_report = None + self._fetch_lock = threading.Lock() def event_deadline(self): now = time.monotonic() @@ -327,14 +333,27 @@ class Daemon: def lock(self): self.locked_until = time.monotonic() + LOCK_SECS + def _fetch_worker(self): + rep = fetch_ocd() + if rep: + with self._fetch_lock: + self._fresh_report = rep + def maybe_tick(self): now = time.monotonic() - if now >= self.next_poll: + # Kick off a poll on a worker thread; never block the main loop. + if now >= self.next_poll and (self._fetch_thread is None or not self._fetch_thread.is_alive()): self.next_poll = now + POLL_SECS - rep = fetch_ocd() + self._fetch_thread = threading.Thread(target=self._fetch_worker, daemon=True) + self._fetch_thread.start() + # Adopt a finished poll's result if one is ready. + if self._fetch_thread is not None and not self._fetch_thread.is_alive(): + self._fetch_thread = None + with self._fetch_lock: + rep = self._fresh_report + self._fresh_report = None if rep: self.report = rep - # keep index in range n = len(rep.get("accounts") or []) if n and self.index >= n: self.index %= n diff --git a/scripts/ocd-waybar-ctl b/scripts/ocd-waybar-ctl index f347f46..2e7f5bb 100755 --- a/scripts/ocd-waybar-ctl +++ b/scripts/ocd-waybar-ctl @@ -1,30 +1,33 @@ #!/bin/sh # ocd-waybar-ctl: send a command to the ocd-waybar daemon. # Usage: ocd-waybar-ctl {advance|lock|poll|quit} -# wired into the waybar custom/ocd module as on-click / on-click-right. +# Wired into the waybar custom/ocd module as on-click / on-click-right. +# +# This must never hang: waybar runs it on a click and stops updating the +# module until it returns. socat's UNIX-SENDTO relay can block waiting on +# the datagram socket, so we use a small python sendto with a hard timeout +# instead. python3 is guaranteed present because the daemon itself needs it. set -eu cmd="${1:-advance}" -sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ocd-waybar.sock" - -if [ "$#" -gt 0 ]; then - cmd="$1" -fi - case "$cmd" in advance|lock|poll|quit) ;; *) echo "ocd-waybar-ctl: unknown command '$cmd'" >&2; exit 2 ;; esac -if [ ! -S "$sock" ]; then - echo "ocd-waybar-ctl: daemon socket not found at $sock" >&2 - exit 1 -fi +sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ocd-waybar.sock" +[ -S "$sock" ] || { echo "ocd-waybar-ctl: daemon socket not found at $sock" >&2; exit 1; } -printf '%s\n' "$cmd" | socat - UNIX-SENDTO:"$sock" 2>/dev/null \ - || printf '%s\n' "$cmd" | nc -U -u -w1 "$sock" 2>/dev/null \ - || python3 - "$sock" "$cmd" <<'PY' +python3 - "$sock" "$cmd" <<'PY' import socket, sys +sock, cmd = sys.argv[1], sys.argv[2] s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) -s.sendto((sys.argv[2] + "\n").encode(), sys.argv[1]) +s.settimeout(2.0) +try: + s.sendto((cmd + "\n").encode(), sock) +except (OSError, socket.timeout) as e: + print("ocd-waybar-ctl: send failed: %s" % e, file=sys.stderr) + sys.exit(1) +finally: + s.close() PY \ No newline at end of file