Compare commits

...

9 commits

Author SHA1 Message Date
29aeee4f49 WIP: Temp disable XDG portal for test 2026-03-17 13:55:37 +00:00
7dabe5a0d2 Format with nixfmt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:11:36 +00:00
43c99ec162 Document USB device passthrough in README and CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:10:54 +00:00
40f726ffcc Add vmsilo-usb to bash completion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:09:35 +00:00
5126a16ffd Add USB systemd services: attach/detach templates, persistent attach, cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:08:45 +00:00
3b4d90bcca Add vmsilo-usb CLI script for runtime USB attach/detach/list
User-facing script that lists USB devices, attaches them to VMs, or
detaches them. Attach/detach dispatch through systemd oneshot templates
for polkit authorization; listing reads sysfs directly (no root needed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:04:28 +00:00
609eccae4a Add USB helper library for state management and crosvm interaction
Sourced by the vmsilo-usb CLI, systemd oneshot services, and persistent
USB attach service. Provides sysfs enumeration, flock-based state file
management, and crosvm usb attach/detach wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:03:57 +00:00
4ef549b981 Add assertion for duplicate USB device assignments across VMs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 12:58:38 +00:00
22259e187f Add usbDevices option for USB device passthrough
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 12:57:33 +00:00
8 changed files with 526 additions and 7 deletions

View file

@ -91,6 +91,7 @@ The configured user can manage VM services via polkit (no sudo required for `vm-
- `vm-stop <name>` — stop VM via systemd (polkit, no sudo)
- `vm-start-debug <name>` — start VM directly for debugging (requires sudo)
- `vm-shell <name>` — connect to VM serial console (default) or SSH with `--ssh`
- `vmsilo-usb [attach|detach] [<name> <vid:pid|devpath>]` — list USB devices, attach/detach USB devices to VMs
See README.md for full usage details and options.

View file

@ -186,6 +186,7 @@ There are a lot of configuration options but you don't really need to touch most
| `sound.capture` | bool | `false` | Enable sound capture (implies playback) |
| `sharedDirectories` | attrsOf submodule | `{}` | Shared directories via virtiofsd (keys are fs tags, see below) |
| `pciDevices` | list of attrsets | `[]` | PCI devices to passthrough (path + optional kv pairs) |
| `usbDevices` | list of attrsets | `[]` | USB devices to passthrough (vendorId, productId, optional serial) |
| `guestPrograms` | list of packages | `[]` | VM-specific packages |
| `guestConfig` | NixOS module(s) | `[]` | VM-specific NixOS configuration (module, list of modules, or path) |
| `vhostUser` | list of attrsets | `[]` | Manual vhost-user devices |
@ -403,6 +404,37 @@ programs.vmsilo = {
boot.blacklistedKernelModules = [ "xhci_hcd" ]; # for USB controllers
```
### USB Passthrough
USB devices can be hot-attached to running VMs individually, without passing through an entire USB controller via PCI passthrough.
#### Configuration
Use the `usbDevices` per-VM option to declare persistent device assignments. Devices are matched by vendor/product ID, optionally narrowed by serial number. All matching physical devices are attached when the VM starts and detached when it stops.
```nix
banking = {
usbDevices = [
{ vendorId = "17ef"; productId = "60e0"; }
{ vendorId = "046d"; productId = "c52b"; serial = "A02019100900"; }
];
};
```
#### Runtime CLI
The `vmsilo-usb` command manages USB device assignments at runtime:
```bash
vmsilo-usb # List all USB devices and which VM they're attached to
vmsilo-usb attach <vm> <vid:pid|devpath> # Attach a device (detaches from current VM if needed)
vmsilo-usb detach <vm> <vid:pid|devpath> # Detach a device from a VM
```
Devices can be identified by `vid:pid` (e.g., `17ef:60e0`) or by sysfs devpath (e.g., `1-2.3`).
Persistent devices configured via `usbDevices` are auto-attached at VM start. All USB state is cleaned up when the VM stops.
### vhost-user Devices
```nix
@ -527,7 +559,7 @@ The host provides:
- Console PTY for serial access (`/run/vmsilo/<name>-console`)
- VM services run as root for PCI passthrough and sandboxing (crosvm drops privileges)
- Polkit rules for the configured user to manage VM services without sudo
- CLI tools: `vm-run`, `vm-start`, `vm-stop`, `vm-start-debug`, `vm-shell`
- CLI tools: `vm-run`, `vm-start`, `vm-stop`, `vm-start-debug`, `vm-shell`, `vmsilo-usb`
- Desktop integration with .desktop files for guest applications
### Tray Integration

View file

@ -182,6 +182,20 @@ in
) effectiveSharedDirs
)
) (lib.attrValues cfg.nixosVms)
# USB device assertions: no duplicate VID:PID+serial across VMs
++ (
let
allUsbDeviceKeys = lib.concatMap (
vm: map (dev: "${dev.vendorId}:${dev.productId}:${toString (dev.serial or "")}") vm.usbDevices
) (lib.attrValues cfg.nixosVms);
in
[
{
assertion = lib.length allUsbDeviceKeys == lib.length (lib.unique allUsbDeviceKeys);
message = "USB devices (VID:PID+serial) cannot be assigned to multiple VMs";
}
]
)
# netvm: network.netvm must reference a VM with isNetvm = true (skip for "host")
++ lib.concatMap (
vm:

View file

@ -602,6 +602,43 @@ let
'';
};
usbDevices = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
vendorId = lib.mkOption {
type = lib.types.str;
description = "USB vendor ID (hex, e.g., '17ef').";
example = "17ef";
};
productId = lib.mkOption {
type = lib.types.str;
description = "USB product ID (hex, e.g., '60e0').";
example = "60e0";
};
serial = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "USB serial number to match a specific device. If null, matches all devices with this VID:PID.";
example = "A02019100900";
};
};
}
);
default = [ ];
description = ''
USB devices to pass through to this VM. Devices are attached when the VM starts
and detached when it stops. Matched by VID:PID, optionally narrowed by serial number.
If multiple physical devices match a VID:PID (and no serial filter), all are attached.
'';
example = lib.literalExpression ''
[
{ vendorId = "17ef"; productId = "60e0"; }
{ vendorId = "046d"; productId = "c52b"; serial = "A02019100900"; }
]
'';
};
guestPrograms = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
@ -935,6 +972,12 @@ in
internal = true;
};
usbHelperLib = lib.mkOption {
type = lib.types.path;
description = "USB helper library script.";
internal = true;
};
# Generated by netvm.nix: maps VM name -> { interfaces, guestConfig }
# Used to inject netvm-derived interfaces and guest config into VMs
# without creating a self-referential cycle on nixosVms.

View file

@ -30,7 +30,7 @@ let
# Bash completions
${lib.optionalString cfg.enableBashIntegration ''
for cmd in vm-run vm-start vm-start-debug vm-stop vm-shell; do
for cmd in vm-run vm-start vm-start-debug vm-stop vm-shell vmsilo-usb; do
ln -s ${cfg._internal.bashCompletionScript} $out/share/bash-completion/completions/$cmd
done
''}

View file

@ -28,6 +28,352 @@ let
vms = assignVmIds cfg.nixosVms;
# USB helper library — sourced by vmsilo-usb CLI, systemd oneshot services, and persistent attach service
usbHelperLib = pkgs.writeShellScript "vmsilo-usb-lib" ''
# --- constants ---
USB_STATE_FILE=/run/vmsilo/usb-state.json
USB_STATE_LOCK=/run/vmsilo/usb-state.lock
CROSVM=${cfg._internal.crosvm}/bin/crosvm
# --- locking (fd 9) ---
usb_lock() {
exec 9>"''${USB_STATE_LOCK}"
${pkgs.util-linux}/bin/flock 9
}
usb_unlock() {
exec 9>&-
}
# --- state file ---
usb_read_state() {
if [ -f "''${USB_STATE_FILE}" ]; then
${pkgs.coreutils}/bin/cat "''${USB_STATE_FILE}"
else
echo '[]'
fi
}
usb_write_state() {
local json="$1"
local tmp="''${USB_STATE_FILE}.tmp.$$"
printf '%s\n' "''${json}" > "''${tmp}"
${pkgs.coreutils}/bin/mv "''${tmp}" "''${USB_STATE_FILE}"
}
# --- enumerate USB devices from sysfs ---
usb_enumerate() {
local result='[]'
for dev in /sys/bus/usb/devices/[0-9]*-[0-9]*; do
[ -d "''${dev}" ] || continue
# Skip interface entries (e.g. 1-3:1.0)
local base
base=$(${pkgs.coreutils}/bin/basename "''${dev}")
case "''${base}" in
*:*) continue ;;
esac
# Read sysfs attributes
local vid pid serial busnum devnum manufacturer product devpath
vid=$(${pkgs.coreutils}/bin/cat "''${dev}/idVendor" 2>/dev/null || echo "")
pid=$(${pkgs.coreutils}/bin/cat "''${dev}/idProduct" 2>/dev/null || echo "")
serial=$(${pkgs.coreutils}/bin/cat "''${dev}/serial" 2>/dev/null || echo "")
busnum=$(${pkgs.coreutils}/bin/cat "''${dev}/busnum" 2>/dev/null || echo "")
devnum=$(${pkgs.coreutils}/bin/cat "''${dev}/devnum" 2>/dev/null || echo "")
manufacturer=$(${pkgs.coreutils}/bin/cat "''${dev}/manufacturer" 2>/dev/null || echo "")
product=$(${pkgs.coreutils}/bin/cat "''${dev}/product" 2>/dev/null || echo "")
devpath="''${base}"
local dev_file="/dev/bus/usb/$(printf '%03d' "''${busnum}")/$(printf '%03d' "''${devnum}")"
[ -z "''${vid}" ] && continue
result=$(printf '%s\n' "''${result}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
--arg vid "''${vid}" \
--arg pid "''${pid}" \
--arg serial "''${serial}" \
--arg busnum "''${busnum}" \
--arg devnum "''${devnum}" \
--arg manufacturer "''${manufacturer}" \
--arg product "''${product}" \
--arg dev_file "''${dev_file}" \
'. + [{devpath: $devpath, vid: $vid, pid: $pid, serial: $serial, busnum: $busnum, devnum: $devnum, manufacturer: $manufacturer, product: $product, dev_file: $dev_file}]')
done
printf '%s\n' "''${result}"
}
# --- find by VID:PID (optional serial) ---
usb_find_by_vidpid() {
local vid="$1" pid="$2" serial="''${3:-}"
local devices
devices=$(usb_enumerate)
if [ -n "''${serial}" ]; then
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
--arg vid "''${vid}" --arg pid "''${pid}" --arg serial "''${serial}" \
'[.[] | select(.vid == $vid and .pid == $pid and .serial == $serial)]'
else
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
--arg vid "''${vid}" --arg pid "''${pid}" \
'[.[] | select(.vid == $vid and .pid == $pid)]'
fi
}
# --- find by devpath ---
usb_find_by_devpath() {
local devpath="$1"
local devices
devices=$(usb_enumerate)
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
'[.[] | select(.devpath == $devpath)]'
}
# --- attach (expects lock already held) ---
usb_do_attach() {
local vm_name="$1" devpath="$2" dev_file="$3" vid="$4" pid="$5" serial="$6" busnum="$7" devnum="$8"
local state socket response port
socket="/run/vmsilo/''${vm_name}-crosvm-control.socket"
state=$(usb_read_state)
# Check if already attached somewhere — detach first
local existing
existing=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
'.[] | select(.devpath == $devpath)')
if [ -n "''${existing}" ]; then
local old_vm old_port old_socket
old_vm=$(printf '%s\n' "''${existing}" | ${pkgs.jq}/bin/jq -r '.vm')
old_port=$(printf '%s\n' "''${existing}" | ${pkgs.jq}/bin/jq -r '.port')
old_socket="/run/vmsilo/''${old_vm}-crosvm-control.socket"
if [ -S "''${old_socket}" ]; then
''${CROSVM} usb detach "''${old_port}" "''${old_socket}" >/dev/null 2>&1 || true
fi
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
'[.[] | select(.devpath != $devpath)]')
fi
# Attach
response=$(''${CROSVM} usb attach "1:1:0000:0000" "''${dev_file}" "''${socket}" 2>&1) || {
echo "Error: crosvm usb attach failed: ''${response}" >&2
usb_write_state "''${state}"
return 1
}
# Parse "ok <port>"
port=$(printf '%s\n' "''${response}" | ${pkgs.gnused}/bin/sed -n 's/^ok \([0-9]*\)/\1/p')
if [ -z "''${port}" ]; then
echo "Error: unexpected crosvm response: ''${response}" >&2
usb_write_state "''${state}"
return 1
fi
# Update state
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" \
--arg devpath "''${devpath}" \
--arg vid "''${vid}" \
--arg pid "''${pid}" \
--arg serial "''${serial}" \
--arg busnum "''${busnum}" \
--arg devnum "''${devnum}" \
--arg port "''${port}" \
'. + [{vm: $vm, devpath: $devpath, vid: $vid, pid: $pid, serial: $serial, busnum: $busnum, devnum: $devnum, port: ($port | tonumber)}]')
usb_write_state "''${state}"
echo "Attached ''${vid}:''${pid} (''${devpath}) to ''${vm_name} on port ''${port}"
}
# --- detach (expects lock already held) ---
usb_do_detach() {
local vm_name="$1" devpath="$2"
local state socket
socket="/run/vmsilo/''${vm_name}-crosvm-control.socket"
state=$(usb_read_state)
local entry
entry=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" --arg devpath "''${devpath}" \
'.[] | select(.vm == $vm and .devpath == $devpath)')
if [ -z "''${entry}" ]; then
echo "Error: device ''${devpath} not attached to ''${vm_name}" >&2
return 1
fi
local port
port=$(printf '%s\n' "''${entry}" | ${pkgs.jq}/bin/jq -r '.port')
if [ -S "''${socket}" ]; then
local response
response=$(''${CROSVM} usb detach "''${port}" "''${socket}" 2>&1) || {
echo "Warning: crosvm usb detach failed: ''${response}" >&2
}
fi
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" --arg devpath "''${devpath}" \
'[.[] | select(.vm != $vm or .devpath != $devpath)]')
usb_write_state "''${state}"
echo "Detached ''${devpath} from ''${vm_name} (port ''${port})"
}
# --- cleanup all entries for a VM (acquires lock) ---
usb_cleanup_vm() {
local vm_name="$1"
usb_lock
local state
state=$(usb_read_state)
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" \
'[.[] | select(.vm != $vm)]')
usb_write_state "''${state}"
usb_unlock
}
'';
# vmsilo-usb CLI — list/attach/detach USB devices to/from VMs
vmsiloUsbScript = pkgs.writeShellScript "vmsilo-usb" ''
set -euo pipefail
# shellcheck source=/dev/null
source ${usbHelperLib}
is_devpath() {
case "$1" in
[0-9]*-[0-9]*) return 0 ;;
*) return 1 ;;
esac
}
parse_vidpid() {
local input="$1"
USB_VID="''${input%%:*}"
USB_PID="''${input#*:}"
if [ -z "''${USB_VID}" ] || [ -z "''${USB_PID}" ] || [ "''${USB_VID}" = "''${input}" ]; then
echo "Error: invalid VID:PID format: ''${input}" >&2
return 1
fi
}
cmd_list() {
local devices state
devices=$(usb_enumerate)
if [ -f "''${USB_STATE_FILE}" ]; then
state=$(usb_read_state)
else
state='[]'
fi
# Merge state into device list and format table
printf '%-10s%-6s%-14s%-14s%-24s%s\n' "VID:PID" "Port" "Serial" "Manufacturer" "Product" "VM"
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -r --argjson state "''${state}" '
.[] |
. as $dev |
($state | map(select(.devpath == $dev.devpath)) | if length > 0 then .[0].vm else "-" end) as $vm |
[$dev.vid + ":" + $dev.pid,
$dev.devpath,
(if $dev.serial == "" then "-" else $dev.serial end),
(if $dev.manufacturer == "" then "-" else $dev.manufacturer end),
(if $dev.product == "" then "-" else $dev.product end),
$vm] |
@tsv
' | while IFS=$'\t' read -r vidpid port serial manufacturer product vm; do
printf '%-10s%-6s%-14s%-14s%-24s%s\n' "''${vidpid}" "''${port}" "''${serial}" "''${manufacturer}" "''${product}" "''${vm}"
done
}
cmd_attach() {
local vm="$1" identifier="$2"
if is_devpath "''${identifier}"; then
local matches
matches=$(usb_find_by_devpath "''${identifier}")
local count
count=$(printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq 'length')
if [ "''${count}" -eq 0 ]; then
echo "Error: no USB device found at ''${identifier}" >&2
exit 1
fi
printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq -r '.[] | [.devpath, .dev_file, .vid, .pid, .serial, .busnum, .devnum] | @tsv' | \
while IFS=$'\t' read -r devpath dev_file vid pid serial busnum devnum; do
local instance
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${devpath}:''${dev_file}:''${vid}:''${pid}:''${serial}:''${busnum}:''${devnum}")
systemctl start "vmsilo-usb-attach@''${instance}.service"
done
else
parse_vidpid "''${identifier}"
local matches
matches=$(usb_find_by_vidpid "''${USB_VID}" "''${USB_PID}")
local count
count=$(printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq 'length')
if [ "''${count}" -eq 0 ]; then
echo "Error: no USB device found matching ''${identifier}" >&2
exit 1
fi
printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq -r '.[] | [.devpath, .dev_file, .vid, .pid, .serial, .busnum, .devnum] | @tsv' | \
while IFS=$'\t' read -r devpath dev_file vid pid serial busnum devnum; do
local instance
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${devpath}:''${dev_file}:''${vid}:''${pid}:''${serial}:''${busnum}:''${devnum}")
systemctl start "vmsilo-usb-attach@''${instance}.service"
done
fi
}
cmd_detach() {
local vm="$1" identifier="$2"
if is_devpath "''${identifier}"; then
local instance
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${identifier}")
systemctl start "vmsilo-usb-detach@''${instance}.service"
else
parse_vidpid "''${identifier}"
local state
state=$(usb_read_state)
local devpaths
devpaths=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -r \
--arg vm "''${vm}" --arg vid "''${USB_VID}" --arg pid "''${USB_PID}" \
'[.[] | select(.vm == $vm and .vid == $vid and .pid == $pid) | .devpath] | .[]')
if [ -z "''${devpaths}" ]; then
echo "Error: no ''${identifier} devices attached to ''${vm}" >&2
exit 1
fi
while IFS= read -r devpath; do
local instance
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${devpath}")
systemctl start "vmsilo-usb-detach@''${instance}.service"
done <<< "''${devpaths}"
fi
}
case "''${1:-}" in
"")
cmd_list
;;
attach)
if [ $# -ne 3 ]; then
echo "Usage: vmsilo-usb attach <vm> <vid:pid|devpath>" >&2
exit 1
fi
cmd_attach "$2" "$3"
;;
detach)
if [ $# -ne 3 ]; then
echo "Usage: vmsilo-usb detach <vm> <vid:pid|devpath>" >&2
exit 1
fi
cmd_detach "$2" "$3"
;;
*)
echo "Usage: vmsilo-usb [attach <vm> <vid:pid|devpath> | detach <vm> <vid:pid|devpath>]" >&2
echo "" >&2
echo "With no arguments, lists all USB devices and their VM assignments." >&2
exit 1
;;
esac
'';
# NOTE: getEffectiveInterfaces is intentionally duplicated in networking.nix and services.nix.
# It cannot live in helpers.nix (which has no config access) and the three modules
# don't share a common let-binding scope. Keep the copies in sync.
@ -593,7 +939,10 @@ in
vm-start-debug = vmStartDebugScript;
vm-stop = vmStopScript;
vm-shell = vmShellScript;
vmsilo-usb = vmsiloUsbScript;
};
usbHelperLib = usbHelperLib;
};
};
}

View file

@ -131,8 +131,13 @@ in
cleanupSocket = pkgs.writeShellScript "cleanup-socket-${vm.name}" ''
rm -f /run/vmsilo/${vm.name}-crosvm-control.socket
'';
usbCleanup = pkgs.writeShellScript "usb-cleanup-${vm.name}" ''
source ${cfg._internal.usbHelperLib}
usb_cleanup_vm "${vm.name}"
'';
stopPostScripts = [
"${cleanupSocket}"
"${usbCleanup}"
]
++ lib.optionals (vm.rootOverlay.type == "raw") [ "${deleteEphemeral}" ];
in
@ -295,6 +300,81 @@ in
};
}
) vms
++ [
# USB attach/detach oneshot template services (invoked by vmsilo-usb CLI)
(lib.nameValuePair "vmsilo-usb-attach@" {
description = "vmsilo USB attach: %I";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.writeShellScript "vmsilo-usb-do-attach" ''
source ${cfg._internal.usbHelperLib}
IFS=':' read -r vm_name devpath dev_file vid pid serial busnum devnum <<< "$1"
usb_lock
usb_do_attach "$vm_name" "$devpath" "$dev_file" "$vid" "$pid" "$serial" "$busnum" "$devnum"
usb_unlock
''} %I";
};
})
(lib.nameValuePair "vmsilo-usb-detach@" {
description = "vmsilo USB detach: %I";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.writeShellScript "vmsilo-usb-do-detach" ''
source ${cfg._internal.usbHelperLib}
IFS=':' read -r vm_name devpath <<< "$1"
usb_lock
usb_do_detach "$vm_name" "$devpath"
usb_unlock
''} %I";
};
})
]
++
# Persistent USB attach services (one per VM with usbDevices)
lib.concatMap (
vm:
lib.optional (vm.usbDevices != [ ]) (
lib.nameValuePair "vmsilo-${vm.name}-usb-attach" {
description = "USB device attach for VM ${vm.name}";
requires = [ "vmsilo-${vm.name}-vm.service" ];
after = [ "vmsilo-${vm.name}-vm.service" ];
wantedBy = [ "vmsilo-${vm.name}-vm.service" ];
bindsTo = [ "vmsilo-${vm.name}-vm.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "vmsilo-usb-attach-${vm.name}" ''
source ${cfg._internal.usbHelperLib}
SOCKET="/run/vmsilo/${vm.name}-crosvm-control.socket"
# Wait for control socket (up to 30s)
ELAPSED=0
while [ ! -S "''${SOCKET}" ] && [ "''${ELAPSED}" -lt 30 ]; do
sleep 0.5
ELAPSED=$((ELAPSED + 1))
done
if [ ! -S "''${SOCKET}" ]; then
echo "Timeout waiting for control socket: ''${SOCKET}" >&2
exit 1
fi
usb_lock
${lib.concatMapStringsSep "\n" (dev: ''
devices=$(usb_find_by_vidpid "${dev.vendorId}" "${dev.productId}" "${toString (dev.serial or "")}")
count=$(echo "''${devices}" | ${pkgs.jq}/bin/jq 'length')
if [ "''${count}" -eq 0 ]; then
echo "Warning: USB device ${dev.vendorId}:${dev.productId} not found" >&2
else
echo "''${devices}" | ${pkgs.jq}/bin/jq -r '.[] | [.devpath, .dev_file, .vid, .pid, .serial, .busnum, .devnum] | @tsv' | \
while IFS=$'\t' read -r devpath dev_file vid pid serial busnum devnum; do
usb_do_attach "${vm.name}" "''${devpath}" "''${dev_file}" "''${vid}" "''${pid}" "''${serial}" "''${busnum}" "''${devnum}" || true
done
fi
'') vm.usbDevices}
usb_unlock
'';
};
}
)
) vms
);
# Session-bind user services: tie GPU VM lifecycle to the desktop session

View file

@ -48,11 +48,11 @@
services.dbus.enable = lib.mkDefault true;
# KDE desktop portal (file dialogs, screen sharing, etc.)
xdg.portal = {
enable = true;
extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
config.common.default = [ "kde" ];
};
#xdg.portal = {
# enable = true;
# extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
# config.common.default = [ "kde" ];
#};
# Enable SSH over vsock (used by `vm-shell --ssh`)
systemd.sockets."sshd-vsock" = {