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:
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) }
|
||||
|
||||
// ── 3–4. 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).
|
||||
|
||||
Reference in New Issue
Block a user