82 lines
2.7 KiB
Python
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
|