import contextlib import os import socket import sys import threading import traceback from pathlib import Path from time import sleep from clan_cli.dirs import vm_state_dir from clan_cli.qemu.qga import QgaSession from clan_cli.qemu.qmp import QEMUMonitorProtocol from . import cli def find_free_port() -> int: """Find an unused localhost port from 1024-65535 and return it.""" with contextlib.closing(socket.socket(type=socket.SOCK_STREAM)) as sock: sock.bind(("127.0.0.1", 0)) return sock.getsockname()[1] def run_vm_in_thread(machine_name: str, ssh_port: int | None = None) -> int: # runs machine and prints exceptions if ssh_port is None: ssh_port = find_free_port() def run() -> None: try: cli.run(["vms", "run", machine_name, "--publish", f"{ssh_port}:22"]) except Exception: # print exception details print(traceback.format_exc(), file=sys.stderr) print(sys.exc_info()[2], file=sys.stderr) # run the machine in a separate thread t = threading.Thread(target=run, name="run") t.daemon = True t.start() return ssh_port # wait for qmp socket to exist def wait_vm_up(machine_name: str, flake_url: str | None = None) -> None: if flake_url is None: flake_url = str(Path.cwd()) socket_file = vm_state_dir(flake_url, machine_name) / "qmp.sock" timeout: float = 600 while True: if timeout <= 0: msg = f"qmp socket {socket_file} not found. Is the VM running?" raise TimeoutError(msg) if socket_file.exists(): break sleep(0.1) timeout -= 0.1 # wait for vm to be down by checking if qmp socket is down def wait_vm_down(machine_name: str, flake_url: str | None = None) -> None: if flake_url is None: flake_url = str(Path.cwd()) socket_file = vm_state_dir(flake_url, machine_name) / "qmp.sock" timeout: float = 300 while socket_file.exists(): if timeout <= 0: msg = f"qmp socket {socket_file} still exists. Is the VM down?" raise TimeoutError(msg) sleep(0.1) timeout -= 0.1 # wait for vm to be up then connect and return qmp instance def qmp_connect(machine_name: str, flake_url: str | None = None) -> QEMUMonitorProtocol: if flake_url is None: flake_url = str(Path.cwd()) state_dir = vm_state_dir(flake_url, machine_name) wait_vm_up(machine_name, flake_url) qmp = QEMUMonitorProtocol( address=str(os.path.realpath(state_dir / "qmp.sock")), ) qmp.connect() return qmp # wait for vm to be up then connect and return qga instance def qga_connect(machine_name: str, flake_url: str | None = None) -> QgaSession: if flake_url is None: flake_url = str(Path.cwd()) state_dir = vm_state_dir(flake_url, machine_name) wait_vm_up(machine_name, flake_url) return QgaSession(os.path.realpath(state_dir / "qga.sock"))