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 committed 2026-07-30 04:43:45 +03:00
1 parent 189a0e03ae
commit c4647dda6b
2 files changed
+40 -18

No files matched your search

+22 -3
View File
@@ -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