mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
218 lines
6.1 KiB
Python
218 lines
6.1 KiB
Python
"""Worker IPC client — connects to the worker server for warm-backend requests.
|
|
|
|
When the worker is available, commands can route through the client for
|
|
faster response times (avoiding cold-start costs). When the worker is
|
|
unavailable, commands fall back to one-shot mode transparently.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import socket
|
|
from typing import Any
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
WORKER_DIR = os.path.join(os.path.expanduser("~"), ".binary-analysis")
|
|
|
|
|
|
def _socket_path() -> str:
|
|
"""Return the path to the worker Unix domain socket."""
|
|
return os.path.join(WORKER_DIR, "worker.sock")
|
|
|
|
|
|
def _pid_path() -> str:
|
|
"""Return the path to the worker PID file."""
|
|
return os.path.join(WORKER_DIR, "worker.pid")
|
|
|
|
|
|
def _started_at_path() -> str:
|
|
"""Return the path to the worker started-at timestamp file."""
|
|
return os.path.join(WORKER_DIR, "worker.started_at")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Worker client
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class WorkerClient:
|
|
"""Client for communicating with the worker IPC server.
|
|
|
|
Usage::
|
|
|
|
client = WorkerClient()
|
|
if client.is_available():
|
|
result = client.send_request({"action": "execute", "command": "metadata", ...})
|
|
# use worker-backed result
|
|
else:
|
|
# fall back to one-shot mode
|
|
"""
|
|
|
|
def __init__(self, timeout: float = 10.0) -> None:
|
|
self._timeout = timeout
|
|
|
|
def is_available(self) -> bool:
|
|
"""Check whether the worker is running and reachable.
|
|
|
|
Returns True if we can connect to the worker socket and get a
|
|
successful ping response.
|
|
"""
|
|
sock_path = _socket_path()
|
|
if not os.path.exists(sock_path):
|
|
return False
|
|
|
|
# Also check that the PID file is valid
|
|
if not _is_pid_alive():
|
|
return False
|
|
|
|
try:
|
|
result = self.send_request({"action": "ping"})
|
|
return result.get("success", False) is True
|
|
except (OSError, ConnectionRefusedError, TimeoutError):
|
|
return False
|
|
|
|
def send_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
"""Send a request to the worker and return the response.
|
|
|
|
Args:
|
|
request: A dict with at minimum an "action" field.
|
|
|
|
Returns:
|
|
The JSON-decoded response dict.
|
|
|
|
Raises:
|
|
OSError: If connection fails.
|
|
TimeoutError: If the connection times out.
|
|
json.JSONDecodeError: If the response is not valid JSON.
|
|
"""
|
|
sock_path = _socket_path()
|
|
if not os.path.exists(sock_path):
|
|
raise OSError(f"Worker socket not found: {sock_path}")
|
|
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.settimeout(self._timeout)
|
|
|
|
try:
|
|
sock.connect(sock_path)
|
|
|
|
# Send request (single JSON line)
|
|
payload = json.dumps(request).encode("utf-8") + b"\n"
|
|
sock.sendall(payload)
|
|
|
|
# Read response (single JSON line)
|
|
response_data = b""
|
|
while b"\n" not in response_data:
|
|
chunk = sock.recv(65536)
|
|
if not chunk:
|
|
break
|
|
response_data += chunk
|
|
|
|
if not response_data:
|
|
raise OSError("Worker closed connection without response")
|
|
|
|
result: dict[str, Any] = json.loads(response_data.decode("utf-8").strip())
|
|
return result
|
|
finally:
|
|
with contextlib.suppress(OSError):
|
|
sock.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Process management helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _is_pid_alive() -> bool:
|
|
"""Check if the PID in the PID file corresponds to a running process."""
|
|
pid_path = _pid_path()
|
|
if not os.path.exists(pid_path):
|
|
return False
|
|
|
|
try:
|
|
with open(pid_path) as f:
|
|
pid_str = f.read().strip()
|
|
if not pid_str:
|
|
return False
|
|
pid = int(pid_str)
|
|
except (ValueError, OSError):
|
|
return False
|
|
|
|
try:
|
|
os.kill(pid, 0)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def read_pid() -> int | None:
|
|
"""Read the worker PID from the PID file.
|
|
|
|
Returns None if the PID file doesn't exist, is empty, or is invalid.
|
|
"""
|
|
pid_path = _pid_path()
|
|
if not os.path.exists(pid_path):
|
|
return None
|
|
|
|
try:
|
|
with open(pid_path) as f:
|
|
pid_str = f.read().strip()
|
|
if not pid_str:
|
|
return None
|
|
return int(pid_str)
|
|
except (ValueError, OSError):
|
|
return None
|
|
|
|
|
|
def read_started_at() -> float | None:
|
|
"""Read the worker started_at timestamp from the file.
|
|
|
|
Returns None if the file doesn't exist or is invalid.
|
|
"""
|
|
path = _started_at_path()
|
|
if not os.path.exists(path):
|
|
return None
|
|
|
|
try:
|
|
with open(path) as f:
|
|
value = f.read().strip()
|
|
if not value:
|
|
return None
|
|
return float(value)
|
|
except (ValueError, OSError):
|
|
return None
|
|
|
|
|
|
def get_worker_status() -> dict[str, Any]:
|
|
"""Get the current worker status.
|
|
|
|
Returns a dict with:
|
|
- state: "running" or "stopped"
|
|
- pid: integer PID when running, null when stopped
|
|
- uptime_seconds: float when running, null when stopped
|
|
"""
|
|
pid = read_pid()
|
|
if pid is not None and _is_pid_alive():
|
|
started_at = read_started_at()
|
|
import time
|
|
|
|
uptime = None
|
|
if started_at is not None:
|
|
uptime = time.monotonic() - started_at
|
|
|
|
return {
|
|
"state": "running",
|
|
"pid": pid,
|
|
"uptime_seconds": round(uptime, 3) if uptime is not None else None,
|
|
}
|
|
else:
|
|
return {
|
|
"state": "stopped",
|
|
"pid": None,
|
|
"uptime_seconds": None,
|
|
}
|