diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index e877bf3b..3010aeed 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -563,73 +563,69 @@ struct Run: AsyncParsableCommand { } private func runUI(_ suspendable: Bool, _ captureSystemKeys: Bool) { - let nsApp = NSApplication.shared - nsApp.setActivationPolicy(.regular) - nsApp.activate(ignoringOtherApps: true) + MainApp.suspendable = suspendable + MainApp.capturesSystemKeys = captureSystemKeys + MainApp.main() + } +} - struct MainApp: App { - static var suspendable: Bool = false - static var capturesSystemKeys: Bool = false - - @NSApplicationDelegateAdaptor private var appDelegate: MinimalMenuAppDelegate - - var body: some Scene { - WindowGroup(vm!.name) { - Group { - VMView(vm: vm!, capturesSystemKeys: MainApp.capturesSystemKeys).onAppear { - NSWindow.allowsAutomaticWindowTabbing = false - }.onDisappear { - let ret = kill(getpid(), MainApp.suspendable ? SIGUSR1 : SIGINT) - if ret != 0 { - // Fallback to the old termination method that doesn't - // propagate the cancellation to Task's in case graceful - // termination via kill(2) is not successful - NSApplication.shared.terminate(self) - } - } - }.frame( - minWidth: CGFloat(vm!.config.display.width), - idealWidth: CGFloat(vm!.config.display.width), - maxWidth: .infinity, - minHeight: CGFloat(vm!.config.display.height), - idealHeight: CGFloat(vm!.config.display.height), - maxHeight: .infinity - ) - }.commands { - // Remove some standard menu options - CommandGroup(replacing: .help, addition: {}) - CommandGroup(replacing: .newItem, addition: {}) - CommandGroup(replacing: .pasteboard, addition: {}) - CommandGroup(replacing: .textEditing, addition: {}) - CommandGroup(replacing: .undoRedo, addition: {}) - CommandGroup(replacing: .windowSize, addition: {}) - // Replace some standard menu options - CommandGroup(replacing: .appInfo) { AboutTart(config: vm!.config) } - CommandMenu("Control") { - Button("Start") { - Task { try await vm!.virtualMachine.start() } - } - Button("Stop") { - Task { try await vm!.virtualMachine.stop() } - } - Button("Request Stop") { - Task { try vm!.virtualMachine.requestStop() } - } - if #available(macOS 14, *) { - if (MainApp.suspendable) { - Button("Suspend") { - kill(getpid(), SIGUSR1) - } - } +struct MainApp: App { + static var suspendable: Bool = false + static var capturesSystemKeys: Bool = false + + @NSApplicationDelegateAdaptor private var appDelegate: MinimalMenuAppDelegate + + var body: some Scene { + WindowGroup(vm!.name) { + Group { + VMView(vm: vm!, capturesSystemKeys: MainApp.capturesSystemKeys).onAppear { + NSWindow.allowsAutomaticWindowTabbing = false + }.onDisappear { + let ret = kill(getpid(), MainApp.suspendable ? SIGUSR1 : SIGINT) + if ret != 0 { + // Fallback to the old termination method that doesn't + // propagate the cancellation to Task's in case graceful + // termination via kill(2) is not successful + NSApplication.shared.terminate(self) + } + } + }.frame( + minWidth: CGFloat(vm!.config.display.width), + idealWidth: CGFloat(vm!.config.display.width), + maxWidth: .infinity, + minHeight: CGFloat(vm!.config.display.height), + idealHeight: CGFloat(vm!.config.display.height), + maxHeight: .infinity + ) + }.commands { + // Remove some standard menu options + CommandGroup(replacing: .help, addition: {}) + CommandGroup(replacing: .newItem, addition: {}) + CommandGroup(replacing: .pasteboard, addition: {}) + CommandGroup(replacing: .textEditing, addition: {}) + CommandGroup(replacing: .undoRedo, addition: {}) + CommandGroup(replacing: .windowSize, addition: {}) + // Replace some standard menu options + CommandGroup(replacing: .appInfo) { AboutTart(config: vm!.config) } + CommandMenu("Control") { + Button("Start") { + Task { try await vm!.virtualMachine.start() } + } + Button("Stop") { + Task { try await vm!.virtualMachine.stop() } + } + Button("Request Stop") { + Task { try vm!.virtualMachine.requestStop() } + } + if #available(macOS 14, *) { + if (MainApp.suspendable) { + Button("Suspend") { + kill(getpid(), SIGUSR1) } } } } } - - MainApp.suspendable = suspendable - MainApp.capturesSystemKeys = captureSystemKeys - MainApp.main() } } @@ -639,6 +635,18 @@ class MinimalMenuAppDelegate: NSObject, NSApplicationDelegate, ObservableObject func applicationDidFinishLaunching(_ : Notification) { NSApplication.shared.mainMenu?.removeItem(at: indexOfEditMenu) + + let nsApp = NSApplication.shared + nsApp.setActivationPolicy(.regular) + nsApp.activate(ignoringOtherApps: true) + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + if (kill(getpid(), MainApp.suspendable ? SIGUSR1 : SIGINT) == 0) { + return .terminateLater + } else { + return .terminateNow + } } } diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 6a6be9d7..9bdc1deb 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -244,10 +244,18 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } func run() async throws { - try await sema.waitUnlessCancelled() + do { + try await sema.waitUnlessCancelled() + } catch is CancellationError { + // Triggered by "tart stop", Ctrl+C, or closing the + // VM window, so shut down the VM gracefully below. + } if Task.isCancelled { - try await stop() + if (self.virtualMachine.state == VZVirtualMachine.State.running) { + print("Stopping VM...") + try await stop() + } } try await network.stop()