nvrsion: Add ability to view the base VM’s monitor during construction for diagnostic purposes, fix stage stuck, and update related views, settings, and core files.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-08 01:44:10 -07:00
co-authored by nucleic
parent 860a047602
commit 5aa61d7604
7 changed files with 87 additions and 2 deletions
@@ -38,6 +38,9 @@ final class MacVMComputerSurface: MacVMSurfaceHost {
let view = VZVirtualMachineView(frame: frame)
view.virtualMachine = vm
view.capturesSystemKeys = true
// Scale with the window so the diagnostic monitor can present at a smaller size (the view
// letterboxes the guest to fit); off-screen it stays at native 1920×1200 for capture/HID.
view.autoresizingMask = [.width, .height]
// Off-screen host window: ordered-in (so the view's Metal layer renders) but positioned far
// off any display, and not made key by default (so it never steals focus). It CAN become key
@@ -81,6 +84,31 @@ final class MacVMComputerSurface: MacVMSurfaceHost {
}
}
func setObserverVisible(name: String, visible: Bool) async {
guard let e = entries[name] else { return }
let w = e.window
if visible {
// Give the (normally off-screen, borderless) HID window real chrome and bring it on-screen
// so the operator can watch and click into a base build that's stuck. The VM view
// scales to fit (autoresizing), so a 1280×800 window shows the whole 1920×1200 guest.
w.styleMask = [.titled, .closable, .resizable, .miniaturizable]
w.title = "Nucleic base VM — diagnostic monitor"
w.setContentSize(NSSize(width: 1280, height: 800))
w.center()
w.makeFirstResponder(e.view)
w.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: true)
} else {
// Back to the invisible, native-size, borderless HID/capture window.
if w.isKeyWindow { w.resignKey() }
w.orderOut(nil)
w.styleMask = [.borderless]
w.setContentSize(NSSize(width: Self.fbWidth, height: Self.fbHeight))
w.setFrameOrigin(NSPoint(x: -30_000, y: -30_000))
w.orderFrontRegardless()
}
}
func cursorPosition(name: String) async -> Point? {
guard let e = entries[name] else { return nil }
return Point(x: Int(e.cursor.x), y: Int(e.cursor.y))
+21
View File
@@ -484,6 +484,8 @@ private struct MacVMSettingsTab: View {
@State private var deleting = false
/// Drives the "Delete base image" destructive confirmation dialog.
@State private var confirmingBaseDelete = false
/// Whether the base-build diagnostic monitor window is currently shown.
@State private var observingBaseVM = false
/// Whether the "Advanced" restore-source overrides are expanded. Driven open by picking the
/// "Custom" base-version option (or by finding a manual override already stored on appearance).
@State private var advancedExpanded = false
@@ -563,6 +565,20 @@ private struct MacVMSettingsTab: View {
detail: phase == baseProgress.phase ? baseProgress.detail : nil)
}
}
Button {
Task {
observingBaseVM.toggle()
await store.setMacVMBaseObserver(visible: observingBaseVM)
}
} label: {
Label(
observingBaseVM ? "Hide VM screen" : "Show VM screen",
systemImage: observingBaseVM ? "eye.slash" : "eye")
}
Text("Opens a live window of the base VM during the build — watch (or click into) a "
+ "build that's stuck, e.g. a first-boot Setup Assistant blocking the automated "
+ "provisioning.")
.settingsCaption()
} else {
Button {
Task { await runBaseBuild() }
@@ -853,6 +869,11 @@ private struct MacVMSettingsTab: View {
let progress = await store.macVMBaseProgress()
recordBuildPhase(progress)
baseProgress = progress
// The build ended (surface detached, monitor auto-hidden) reset the toggle to match.
if progress == nil, observingBaseVM {
observingBaseVM = false
await store.setMacVMBaseObserver(visible: false)
}
runningVMs = await store.runningMacVMs()
baseOSVersion = await store.macVMBaseOSVersion()
baseStatus = await store.macVMBaseStatus()
+6
View File
@@ -1750,6 +1750,12 @@ public final class AppStore: ConflictArbiter {
try await macVMManager.deleteBaseImage(includingRestoreImages: includingRestoreImages)
}
/// Show or hide the diagnostic monitor window for the in-flight base build a live
/// `VZVirtualMachineView` of the provisioning guest, for watching a build that's stuck.
public func setMacVMBaseObserver(visible: Bool) async {
await macVMManager?.setBaseProvisionObserver(visible: visible)
}
public func renameProject(_ id: ProjectID, to name: String) async {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, var project = projectsByID[id] else { return }
@@ -213,7 +213,8 @@ extension MacVMEngine {
let config = try Self.makeConfiguration(
bundle: bundle, cpus: MacVMSettings.vmCPUs, memoryGiB: MacVMSettings.vmMemoryGiB,
mac: mac, mounts: [mount])
let instance = MacVMInstance(configuration: config, label: "base-provision", mainQueue: true)
let instance = MacVMInstance(
configuration: config, label: Self.baseProvisionSurfaceName, mainQueue: true)
do {
if declarativeFirstBoot, #available(macOS 27.0, *) {
try await instance.startWithProvisioning(
@@ -226,10 +227,12 @@ extension MacVMEngine {
provisioned: false, agentInstalled: false, sipDisabled: false, axAgentReady: false)
}
let surfaceName = "base-provision"
let surfaceName = Self.baseProvisionSurfaceName
await surface.attach(
name: surfaceName,
virtualMachine: UncheckedSendableBox(value: instance.vm as AnyObject))
// If the operator opted to watch this build, bring the monitor up now that the view is bound.
if baseObserverRequested { await surface.setObserverVisible(name: surfaceName, visible: true) }
// 34. Drive HID launch the bootstrap poll STATUS on the share.
let activePhase: MacVMBaseProgress.Phase =
@@ -261,6 +264,17 @@ extension MacVMEngine {
/// The staged bootstrap's filename on the share.
static let bootstrapScriptName = "nucleic-bootstrap.sh"
/// Surface (and VM) name for the base-build provisioning VM the key the diagnostic monitor and
/// the HID bootstrap both address.
static let baseProvisionSurfaceName = "base-provision"
/// Show or hide the diagnostic monitor window for the in-flight base build. Remembers the request
/// so it's (re)applied when the provisioning surface next attaches; a no-op when no build is live.
public func setBaseProvisionObserver(visible: Bool) async {
baseObserverRequested = visible
await surfaceHost?.setObserverVisible(name: Self.baseProvisionSurfaceName, visible: visible)
}
/// Drive the host-side surface to launch the staged bootstrap, then poll the share for its
/// `STATUS` readback. Re-attempts the launch (open Spotlight Terminal run) until a `STARTED`
/// sentinel proves the bootstrap is running (so we stop typing into a live install), then just
@@ -88,6 +88,9 @@ public actor MacVMEngine {
/// ``beginBaseRecovery()``/``endBaseRecovery()``. Both flags block cutting a fresh session clone
/// from the base, since cloning reads the base disk that would then be mid-write.
var baseRecoveryActive = false
/// Whether the operator asked to watch the base-build VM in a diagnostic monitor window. Applied
/// when the provisioning surface attaches, and toggled live via ``setBaseProvisionObserver(visible:)``.
var baseObserverRequested = false
/// The base disk is being written (a build/provisioning pass, or a Recovery session), so it must
/// not be read for a copy-on-write clone right now (a torn read would corrupt the clone).
@@ -190,6 +190,11 @@ public actor MacVMManager {
try await engine.deleteBaseImage(includingRestoreImages: includingRestoreImages)
}
/// Show or hide the diagnostic monitor window for the in-flight base build (watch a stuck build).
public func setBaseProvisionObserver(visible: Bool) async {
await engine.setBaseProvisionObserver(visible: visible)
}
/// On launch, reconcile on-disk clone bundles keep only the active sessions' clones (daemonless,
/// so this is on-disk GC).
public func reconcile(activeSessions: [SessionID]) async {
@@ -31,12 +31,20 @@ public protocol MacVMSurfaceHost: AnyObject, Sendable {
/// by the one-time base-build HID bootstrap (which types a command into the guest's Terminal);
/// per-session computer-use VMs never call it. Default: no-op.
func setKeyboardFocus(name: String, focused: Bool) async
/// Show (`visible: true`) or hide the surface's window on-screen as a **diagnostic monitor** for
/// `name`. The surface normally renders off-screen (framebuffer capture + HID only); this brings
/// the live `VZVirtualMachineView` into a titled window so an operator can watch and, if needed,
/// click into a base build that's misbehaving. On-screen rendering is live even when programmatic
/// framebuffer *captures* come back blank (a known recent-guest quirk). Default: no-op.
func setObserverVisible(name: String, visible: Bool) async
}
extension MacVMSurfaceHost {
/// Surfaces that don't need explicit key management (e.g. the `macvm-spike` harness, whose lone
/// window becomes key on its own) inherit a no-op.
public func setKeyboardFocus(name: String, focused: Bool) async {}
/// Surfaces without a diagnostic-monitor concept inherit a no-op.
public func setObserverVisible(name: String, visible: Bool) async {}
}
/// A guest-space point (1920×1200 framebuffer coordinates).