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).
This commit is contained in:
ocpway 2026-07-30 04:43:45 +03:00
parent 189a0e03ae
commit c4647dda6b
2 changed files with 40 additions and 18 deletions

View File

@ -20,6 +20,7 @@ import signal
import socket import socket
import subprocess import subprocess
import sys import sys
import threading
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from html import escape from html import escape
@ -303,6 +304,11 @@ class Daemon:
self.last_key = None self.last_key = None
self.sock = None self.sock = None
self.running = True 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): def event_deadline(self):
now = time.monotonic() now = time.monotonic()
@ -327,14 +333,27 @@ class Daemon:
def lock(self): def lock(self):
self.locked_until = time.monotonic() + LOCK_SECS self.locked_until = time.monotonic() + LOCK_SECS
def maybe_tick(self): def _fetch_worker(self):
now = time.monotonic()
if now >= self.next_poll:
self.next_poll = now + POLL_SECS
rep = fetch_ocd() rep = fetch_ocd()
if rep:
with self._fetch_lock:
self._fresh_report = rep
def maybe_tick(self):
now = time.monotonic()
# 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
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: if rep:
self.report = rep self.report = rep
# keep index in range
n = len(rep.get("accounts") or []) n = len(rep.get("accounts") or [])
if n and self.index >= n: if n and self.index >= n:
self.index %= n self.index %= n

View File

@ -1,30 +1,33 @@
#!/bin/sh #!/bin/sh
# ocd-waybar-ctl: send a command to the ocd-waybar daemon. # ocd-waybar-ctl: send a command to the ocd-waybar daemon.
# Usage: ocd-waybar-ctl {advance|lock|poll|quit} # 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 set -eu
cmd="${1:-advance}" cmd="${1:-advance}"
sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ocd-waybar.sock"
if [ "$#" -gt 0 ]; then
cmd="$1"
fi
case "$cmd" in case "$cmd" in
advance|lock|poll|quit) ;; advance|lock|poll|quit) ;;
*) echo "ocd-waybar-ctl: unknown command '$cmd'" >&2; exit 2 ;; *) echo "ocd-waybar-ctl: unknown command '$cmd'" >&2; exit 2 ;;
esac esac
if [ ! -S "$sock" ]; then sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ocd-waybar.sock"
echo "ocd-waybar-ctl: daemon socket not found at $sock" >&2 [ -S "$sock" ] || { echo "ocd-waybar-ctl: daemon socket not found at $sock" >&2; exit 1; }
exit 1
fi
printf '%s\n' "$cmd" | socat - UNIX-SENDTO:"$sock" 2>/dev/null \ python3 - "$sock" "$cmd" <<'PY'
|| printf '%s\n' "$cmd" | nc -U -u -w1 "$sock" 2>/dev/null \
|| python3 - "$sock" "$cmd" <<'PY'
import socket, sys import socket, sys
sock, cmd = sys.argv[1], sys.argv[2]
s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 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 PY