diff --git a/pkgs/clan-cli/clan_cli/cmd.py b/pkgs/clan-cli/clan_cli/cmd.py index ad1fdd51..c14a14da 100644 --- a/pkgs/clan-cli/clan_cli/cmd.py +++ b/pkgs/clan-cli/clan_cli/cmd.py @@ -43,14 +43,14 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]: return b"" ret = handle_fd(process.stdout) - if log in [Log.STDOUT, Log.BOTH]: + if ret and log in [Log.STDOUT, Log.BOTH]: sys.stdout.buffer.write(ret) sys.stdout.flush() stdout_buf += ret ret = handle_fd(process.stderr) - if log in [Log.STDERR, Log.BOTH]: + if ret and log in [Log.STDERR, Log.BOTH]: sys.stderr.buffer.write(ret) sys.stderr.flush() stderr_buf += ret diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index fd75bf29..b4bf26e8 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -86,7 +86,3 @@ def nixpkgs_flake() -> Path: def nixpkgs_source() -> Path: return (module_root() / "nixpkgs" / "path").resolve() - - -def unfree_nixpkgs() -> Path: - return module_root() / "nixpkgs" / "unfree" diff --git a/pkgs/clan-cli/clan_cli/vms/run.py b/pkgs/clan-cli/clan_cli/vms/run.py index 42a30400..075e67a3 100644 --- a/pkgs/clan-cli/clan_cli/vms/run.py +++ b/pkgs/clan-cli/clan_cli/vms/run.py @@ -1,9 +1,15 @@ import argparse +import contextlib import importlib import json import logging import os +import random +import socket +import subprocess import tempfile +import time +from collections.abc import Iterator from dataclasses import dataclass, field from pathlib import Path from typing import IO @@ -19,21 +25,38 @@ from .inspect import VmConfig, inspect_vm log = logging.getLogger(__name__) -def graphics_options(vm: VmConfig) -> list[str]: - common = ["-audio", "driver=pa,model=virtio"] +@dataclass +class GraphicOptions: + args: list[str] + vsock_cid: int | None = None + + +def graphics_options(vm: VmConfig) -> GraphicOptions: + common = [ + "-audio", + "driver=pa,model=virtio", + ] if vm.wayland: + # FIXME: check for collisions + cid = random.randint(1, 2**32) # fmt: off - return [ + return GraphicOptions([ *common, "-nographic", "-vga", "none", - "-device", "virtio-gpu-rutabaga,gfxstream-vulkan=on,cross-domain=on,hostmem=4G,wsi=headless", - ] + "-device", f"vhost-vsock-pci,id=vhost-vsock-pci0,guest-cid={cid}", + # TODO: vgpu + #"-display", "egl-headless,gl=core", + #"-device", "virtio-vga,blob=true", + #"-device", "virtio-serial-pci", + #"-device", "vhost-user-vga,chardev=vgpu", + #"-chardev", "socket,id=vgpu,path=/tmp/vgpu.sock", + ], cid) # fmt: on else: # fmt: off - return [ + return GraphicOptions([ *common, "-vga", "none", "-display", "gtk,gl=on", @@ -52,10 +75,16 @@ def graphics_options(vm: VmConfig) -> list[str]: "-device", "pci-ohci,id=smartpass", "-device", "usb-ccid", "-chardev", "spicevmc,id=ccid,name=smartcard", - ] + ], None) # fmt: on +@dataclass +class QemuCommand: + args: list[str] + vsock_cid: int | None = None + + def qemu_command( vm: VmConfig, nixos_config: dict[str, str], @@ -63,7 +92,7 @@ def qemu_command( secrets_dir: Path, state_dir: Path, disk_img: Path, -) -> list[str]: +) -> QemuCommand: kernel_cmdline = [ (Path(nixos_config["toplevel"]) / "kernel-params").read_text(), f'init={nixos_config["toplevel"]}/init', @@ -77,6 +106,8 @@ def qemu_command( "qemu-kvm", "-name", vm.machine_name, "-m", f'{nixos_config["memorySize"]}M', + "-object", f"memory-backend-memfd,id=mem,size={nixos_config['memorySize']}M", + "-machine", "pc,memory-backend=mem,accel=kvm", "-smp", str(nixos_config["cores"]), "-cpu", "max", "-enable-kvm", @@ -97,11 +128,14 @@ def qemu_command( "-append", " ".join(kernel_cmdline), ] # fmt: on + vsock_cid = None if vm.graphics: - command.extend(graphics_options(vm)) + opts = graphics_options(vm) + vsock_cid = opts.vsock_cid + command.extend(opts.args) else: command.append("-nographic") - return command + return QemuCommand(command, vsock_cid=vsock_cid) # TODO move this to the Machines class @@ -185,6 +219,45 @@ def prepare_disk(tmpdir: Path, log_fd: IO[str] | None) -> Path: return disk_img +VMADDR_CID_HYPERVISOR = 2 + + +def test_vsock_port(port: int) -> bool: + try: + s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM) + s.connect((VMADDR_CID_HYPERVISOR, port)) + s.close() + return True + except OSError: + return False + + +@contextlib.contextmanager +def start_waypipe(cid: int | None, title_prefix: str) -> Iterator[None]: + if cid is None: + yield + return + waypipe = nix_shell( + ["git+https://git.clan.lol/clan/clan-core#waypipe"], + [ + "waypipe", + "--vsock", + "--socket", + f"s{cid}:3049", + "--title-prefix", + title_prefix, + "client", + ], + ) + with subprocess.Popen(waypipe) as proc: + try: + while not test_vsock_port(3049): + time.sleep(0.1) + yield + finally: + proc.kill() + + def run_vm( vm: VmConfig, nix_options: list[str] = [], @@ -229,12 +302,13 @@ def run_vm( "XDG_DATA_DIRS" ] = f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}" - run( - nix_shell(packages, qemu_cmd), - env=env, - log=Log.BOTH, - error_msg=f"Could not start vm {machine}", - ) + with start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "): + run( + nix_shell(packages, qemu_cmd.args), + env=env, + log=Log.BOTH, + error_msg=f"Could not start vm {machine}", + ) @dataclass diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 6bb6a649..2b256e8c 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -31,7 +31,6 @@ , mypy , rope , clan-core-path -, writeShellScriptBin }: let @@ -40,8 +39,6 @@ let ]; pytestDependencies = runtimeDependencies ++ dependencies ++ [ - - #schemathesis # optional for http fuzzing pytest pytest-cov pytest-subprocess