Brad Fitzpatrick 674f866ecc tstest/tailmac: add headless mode for automated VM testing
Add a --headless flag to the Host.app Run subcommand for running
macOS VMs without a GUI, enabling use from test frameworks.

Key changes:

  - HostCli.swift: When --headless is set, run the VM via VMController
    + RunLoop.main.run() instead of NSApplicationMain. Using the
    RunLoop (not dispatchMain) is required because VZ framework
    callbacks depend on RunLoop sources.

  - VMController.swift: Add headless parameter to createVirtualMachine
    that configures a single socket-based NIC (no NAT NIC). This
    matches the NIC configuration used when creating/saving VMs, so
    saved state restoration works correctly. A NIC count mismatch
    causes VZ to silently fail to execute guest code.

  - TailMacConfigHelper.swift: Clean up socket network device logging.

  - Config.swift: Move VM storage from ~/VM.bundle to
    ~/.cache/tailscale/vmtest/macos/.

  - TailMac.swift: Fix dispatchMain→RunLoop.main.run() in the create
    command (same VZ RunLoop requirement).

Updates #13038

Change-Id: Iea51c043aa92e8fc6257139b9f0e2e7677072fa2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-11 12:50:53 -07:00

52 lines
1.6 KiB
Swift

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
import Cocoa
import Foundation
import Virtualization
import ArgumentParser
@main
struct HostCli: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "A utility for running virtual machines",
subcommands: [Run.self],
defaultSubcommand: Run.self)
}
var config: Config = Config()
extension HostCli {
struct Run: ParsableCommand {
@Option var id: String
@Option var share: String?
@Flag(help: "Run without GUI (for automated testing)") var headless: Bool = false
mutating func run() {
config = Config(id)
config.sharedDir = share
print("Running vm with identifier \(id) and sharedDir \(share ?? "<none>")")
if headless {
DispatchQueue.main.async {
let controller = VMController()
controller.createVirtualMachine(headless: true)
let fileManager = FileManager.default
if fileManager.fileExists(atPath: config.saveFileURL.path) {
print("Restoring virtual machine state from \(config.saveFileURL)")
controller.restoreVirtualMachine()
} else {
print("Starting virtual machine")
controller.startVirtualMachine()
}
}
RunLoop.main.run()
} else {
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
}
}
}
}