#!/usr/bin/env bash # # qvm-start - Launch the QEMU VM with all required configuration # # This script starts the QVM virtual machine with: # - KVM acceleration and host CPU passthrough # - Configurable memory and CPU count # - Overlay disk backed by base.qcow2 (copy-on-write) # - SSH port forwarding on auto-selected port # - 9p mounts for shared caches (cargo, pnpm, sccache) # - Serial console logging # - Daemonized execution with PID file # set -euo pipefail # Source common library QVM_LIB_DIR="${QVM_LIB_DIR:-$(dirname "$(readlink -f "$0")")/../lib}" # shellcheck source=lib/common.sh source "$QVM_LIB_DIR/common.sh" # # find_available_port - Find an available TCP port starting from base # Args: $1 - starting port number (default: 2222) # Returns: available port number on stdout # find_available_port() { local port="${1:-2222}" local max_attempts=100 local attempt=0 while (( attempt < max_attempts )); do if ! nc -z localhost "$port" 2>/dev/null; then echo "$port" return 0 fi (( port++ )) (( attempt++ )) done die "Could not find available port after $max_attempts attempts" } # # mount_workspaces - Add virtfs entries for registered workspaces # Args: $1 - name of array variable to append to # Usage: mount_workspaces qemu_cmd # mount_workspaces() { local -n cmd_array=$1 # Check if workspaces registry exists if [[ ! -f "$QVM_WORKSPACES_FILE" ]]; then log_info "No workspaces registry found, skipping workspace mounts" return 0 fi # Check if file is empty or invalid JSON if [[ ! -s "$QVM_WORKSPACES_FILE" ]]; then log_info "Workspaces registry is empty, skipping workspace mounts" return 0 fi # Parse workspaces and add virtfs entries local workspace_count workspace_count=$(jq -r 'length' "$QVM_WORKSPACES_FILE" 2>/dev/null || echo "0") if [[ "$workspace_count" -eq 0 ]]; then log_info "No workspaces registered, skipping workspace mounts" return 0 fi log_info "Mounting $workspace_count workspace(s)..." # Iterate through workspaces and add virtfs entries local i=0 while (( i < workspace_count )); do local path mount_tag path=$(jq -r ".[$i].path" "$QVM_WORKSPACES_FILE") mount_tag=$(jq -r ".[$i].mount_tag" "$QVM_WORKSPACES_FILE") if [[ -z "$path" || -z "$mount_tag" || "$path" == "null" || "$mount_tag" == "null" ]]; then log_warn "Skipping invalid workspace entry at index $i" (( i++ )) continue fi # Verify path exists if [[ ! -d "$path" ]]; then log_warn "Workspace path does not exist: $path (skipping)" (( i++ )) continue fi log_info " - $path -> $mount_tag" cmd_array+=(-virtfs "local,path=$path,mount_tag=$mount_tag,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600") (( i++ )) done } # # cleanup_on_failure - Clean up state files if VM start fails # cleanup_on_failure() { log_warn "Cleaning up after failed start..." rm -f "$QVM_PID_FILE" "$QVM_SSH_PORT_FILE" } # # main - Main execution flow # main() { log_info "Starting QVM..." # Check if VM is already running if is_vm_running; then log_info "VM is already running" local port port=$(get_ssh_port) echo "SSH available on port: $port" echo "Use 'qvm ssh' to connect or 'qvm status' for details" exit 0 fi # First-run initialization ensure_dirs if [[ ! -f "$QVM_BASE_IMAGE" ]]; then log_info "First run detected - building base image..." log_info "This may take several minutes." # Call qvm-rebuild to build the image SCRIPT_DIR="$(dirname "$0")" if ! "$SCRIPT_DIR/qvm-rebuild"; then die "Failed to build base image. Run 'qvm rebuild' manually to debug." fi fi # Create overlay image if it doesn't exist if [[ ! -f "$QVM_OVERLAY" ]]; then log_info "Creating overlay disk..." if ! qemu-img create -f qcow2 -b "$QVM_BASE_IMAGE" -F qcow2 "$QVM_OVERLAY"; then die "Failed to create overlay disk" fi else log_info "Using existing overlay disk" fi # Find available SSH port local ssh_port ssh_port=$(find_available_port 2222) log_info "Using SSH port: $ssh_port" # Get memory and CPU settings from environment or use defaults local memory="${QVM_MEMORY:-8G}" local cpus="${QVM_CPUS:-4}" log_info "VM resources: ${memory} memory, ${cpus} CPUs" # Build QEMU command local qemu_cmd=( qemu-system-x86_64 -enable-kvm -cpu host -m "$memory" -smp "$cpus" # Overlay disk (virtio for performance) -drive "file=$QVM_OVERLAY,if=virtio,format=qcow2" # User-mode networking with SSH port forward -netdev "user,id=net0,hostfwd=tcp::${ssh_port}-:22" -device "virtio-net-pci,netdev=net0" # 9p mounts for shared caches (security_model=mapped-xattr for proper permissions) -virtfs "local,path=$QVM_CARGO_HOME,mount_tag=cargo_home,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600" -virtfs "local,path=$QVM_CARGO_TARGET,mount_tag=cargo_target,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600" -virtfs "local,path=$QVM_PNPM_STORE,mount_tag=pnpm_store,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600" -virtfs "local,path=$QVM_SCCACHE,mount_tag=sccache,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600" ) # Add workspace mounts from registry mount_workspaces qemu_cmd # Continue building QEMU command qemu_cmd+=( # Serial console to log file -serial "file:$QVM_SERIAL_LOG" # No graphics -nographic # Daemonize with PID file -daemonize -pidfile "$QVM_PID_FILE" ) # Launch QEMU log_info "Launching QEMU..." if ! "${qemu_cmd[@]}"; then cleanup_on_failure die "Failed to start QEMU" fi # Save SSH port to file echo "$ssh_port" > "$QVM_SSH_PORT_FILE" # Wait for SSH to become available if ! wait_for_ssh "$ssh_port" 60; then cleanup_on_failure die "VM started but SSH did not become available" fi # Success! log_info "VM started successfully" echo "" echo "SSH available on port: $ssh_port" echo "Connect with: qvm ssh" echo "Check status: qvm status" echo "Serial log: $QVM_SERIAL_LOG" } # Run main function main "$@"