#!/usr/bin/env python3 """Non-blocking Unix Socket client for Sidecar daemon. Uses GLib.io_add_watch for async I/O — no threading needed. Protocol: Send JSON command, receive JSON response, close. """ import json import socket from pathlib import Path from gi.repository import GLib SOCKET_PATH = "/tmp/sidecar-v7.sock" def send_command(cmd_data: dict, callback, error_callback=None) -> None: """Send a command to the sidecar daemon asynchronously. Args: cmd_data: Dict with at least {"cmd": "..."}. callback: Called with parsed JSON response dict. error_callback: Called with error message string on failure. """ # V6 protocol: wrap as {"type": "ctl", "cmd": "..."} cmd_data["type"] = "ctl" raw = json.dumps(cmd_data).encode() try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.setblocking(False) except OSError as e: if error_callback: error_callback(f"Socket creation failed: {e}") return try: sock.connect(SOCKET_PATH) except BlockingIOError: pass # connect in progress — normal for non-blocking except (FileNotFoundError, ConnectionRefusedError) as e: sock.close() if error_callback: error_callback("Daemon not running") return except OSError as e: sock.close() if error_callback: error_callback(str(e)) return chunks = [] sent = [False] def _on_io(fd, condition): if condition & GLib.IO_OUT and not sent[0]: try: sock.sendall(raw) sock.shutdown(socket.SHUT_WR) # Signal EOF so daemon stops reading sent[0] = True except OSError as e: sock.close() if error_callback: GLib.idle_add(error_callback, str(e)) return False # Now watch for incoming data GLib.io_add_watch(sock.fileno(), GLib.IO_IN | GLib.IO_HUP | GLib.IO_ERR, _on_read) return False # remove OUT watch return False def _on_read(fd, condition): # IMMER zuerst lesen wenn IO_IN gesetzt — auch wenn IO_HUP gleichzeitig kommt if condition & GLib.IO_IN: try: chunk = sock.recv(65536) if chunk: chunks.append(chunk) return True # weiter lesen # chunk empty = EOF → finish below except BlockingIOError: return True # no data yet, keep watch (GLib won't spin — IO_IN only fires on readability) except OSError: pass # Erst wenn keine Daten mehr kommen (EOF, HUP, ERR): finish _finish() return False def _finish(): sock.close() raw_resp = b"".join(chunks).decode(errors="replace") if not raw_resp: if error_callback: GLib.idle_add(error_callback, "Empty response") return try: data = json.loads(raw_resp) GLib.idle_add(callback, data) except json.JSONDecodeError as e: if error_callback: GLib.idle_add(error_callback, f"Invalid JSON: {e}") GLib.io_add_watch(sock.fileno(), GLib.IO_OUT, _on_io) def send_ping(callback, error_callback=None) -> None: """Send a ping to check daemon health (V6 protocol).""" raw = json.dumps({"type": "ping"}).encode() try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.setblocking(False) except OSError as e: if error_callback: error_callback(f"Socket creation failed: {e}") return try: sock.connect(SOCKET_PATH) except BlockingIOError: pass except (FileNotFoundError, ConnectionRefusedError): sock.close() if error_callback: error_callback("Daemon not running") return except OSError as e: sock.close() if error_callback: error_callback(str(e)) return chunks = [] sent = [False] def _on_io(fd, condition): if condition & GLib.IO_OUT and not sent[0]: try: sock.sendall(raw) sock.shutdown(socket.SHUT_WR) sent[0] = True except OSError as e: sock.close() if error_callback: GLib.idle_add(error_callback, str(e)) return False GLib.io_add_watch(sock.fileno(), GLib.IO_IN | GLib.IO_HUP | GLib.IO_ERR, _on_read) return False return False def _on_read(fd, condition): if condition & GLib.IO_IN: try: chunk = sock.recv(65536) if chunk: chunks.append(chunk) return True except BlockingIOError: return True except OSError: pass _finish() return False def _finish(): sock.close() raw_resp = b"".join(chunks).decode(errors="replace") if not raw_resp: if error_callback: GLib.idle_add(error_callback, "Empty response") return try: data = json.loads(raw_resp) GLib.idle_add(callback, data) except json.JSONDecodeError as e: if error_callback: GLib.idle_add(error_callback, f"Invalid JSON: {e}") GLib.io_add_watch(sock.fileno(), GLib.IO_OUT, _on_io) def is_daemon_reachable() -> bool: """Check if daemon is reachable by attempting a real socket connection.""" import socket as _socket try: s = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM) s.settimeout(0.5) s.connect(SOCKET_PATH) s.close() return True except OSError: return False