Files
owui-tools/vm/vmd/client.py
T
2026-09-15 15:24:03 -04:00

82 lines
2.7 KiB
Python

"""Client for vmd's unix socket.
Newline-delimited JSON, one request per line, one reply per line. There
is no framing beyond that and no session state in the protocol - vmd
holds the session, because there is only ever one.
Used by two callers: `vmctl` on the host, for driving a session by hand
with no model in the loop, and the agent container, where each verb
becomes a tool the sub-agent can call. Keeping them on the same client is
deliberate: whatever the agent can do, a person can do by hand in the
same order, which is what makes a misbehaving session reproducible.
"""
from __future__ import annotations
import json
import socket
from typing import Any
SOCKET_PATH = "/run/owuivm/agent/agent.sock"
class VmdError(RuntimeError):
"""vmd refused a verb. The message is written for a model to read."""
class VmdUnavailable(RuntimeError):
"""vmd is not running, or the socket is not reachable from here."""
class Vmd:
def __init__(self, path: str = SOCKET_PATH, timeout: float = 1800.0) -> None:
self.path = path
self.timeout = timeout
self._sock: socket.socket | None = None
self._buf = b""
def __enter__(self) -> "Vmd":
self.connect()
return self
def __exit__(self, *_exc: object) -> None:
self.close()
def connect(self) -> None:
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
sock.connect(self.path)
except OSError as exc:
raise VmdUnavailable(
f"cannot reach the VM daemon at {self.path}: {exc}. "
"It is not running, or this process is not in the group that "
"may talk to it."
) from exc
self._sock = sock
def close(self) -> None:
if self._sock is not None:
try:
self._sock.close()
finally:
self._sock = None
def call(self, verb: str, **args: Any) -> dict[str, Any]:
if self._sock is None:
self.connect()
assert self._sock is not None
self._sock.sendall(json.dumps({"verb": verb, "args": args}).encode() + b"\n")
while True:
line, sep, rest = self._buf.partition(b"\n")
if sep:
self._buf = rest
reply = json.loads(line)
if not reply.get("ok"):
raise VmdError(reply.get("error", "the VM daemon refused, with no reason given"))
return reply.get("result") or {}
chunk = self._sock.recv(1 << 20)
if not chunk:
raise VmdUnavailable("the VM daemon closed the connection")
self._buf += chunk