Initial Commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user