Download Sidecar/gui/socket_client.py from smlflg/Sidecar-Workspace: direct link, hf CLI and curl.
- Browser
- Download file 5.96 kB
-
https://huggingface.co/smlflg/Sidecar-Workspace/resolve/main/Sidecar/gui/socket_client.py
- Command line
-
hf download hf://smlflg/Sidecar-Workspace/Sidecar/gui/socket_client.py
-
curl -L -o socket_client.py https://huggingface.co/smlflg/Sidecar-Workspace/resolve/main/Sidecar/gui/socket_client.py
5.96 kB
| #!/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 | |