diff --git a/SecVF.xcodeproj/project.pbxproj b/SecVF.xcodeproj/project.pbxproj index 3a2f009..cfd4186 100644 --- a/SecVF.xcodeproj/project.pbxproj +++ b/SecVF.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ THBT002THBT002THBT002002 /* TacticalHoverButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = THBT001THBT001THBT001001 /* TacticalHoverButton.swift */; }; TTRV002TTRV002TTRV002002 /* TacticalTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = TTRV001TTRV001TTRV001001 /* TacticalTableRowView.swift */; }; TESV002TESV002TESV002002 /* TacticalEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = TESV001TESV001TESV001001 /* TacticalEmptyStateView.swift */; }; + ICMW002ICMW002ICMW002002 /* ISOCacheManagerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ICMW001ICMW001ICMW001001 /* ISOCacheManagerWindow.swift */; }; + SSWC002SSWC002SSWC002002 /* SwitchStatisticsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = SSWC001SSWC001SSWC001001 /* SwitchStatisticsWindowController.swift */; }; TSSC002TSSC002TSSC002002 /* TacticalSidebarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = TSSC001TSSC001TSSC001001 /* TacticalSidebarSection.swift */; }; TTHC002TTHC002TTHC002002 /* TacticalTableHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = TTHC001TTHC001TTHC001001 /* TacticalTableHeaderCell.swift */; }; PCPR002PCPR002PCPR002002 /* PacketCaptureProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = PCPR001PCPR001PCPR001001 /* PacketCaptureProtocol.swift */; }; @@ -167,6 +169,8 @@ THBT001THBT001THBT001001 /* TacticalHoverButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalHoverButton.swift; sourceTree = ""; }; TTRV001TTRV001TTRV001001 /* TacticalTableRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalTableRowView.swift; sourceTree = ""; }; TESV001TESV001TESV001001 /* TacticalEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalEmptyStateView.swift; sourceTree = ""; }; + ICMW001ICMW001ICMW001001 /* ISOCacheManagerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ISOCacheManagerWindow.swift; sourceTree = ""; }; + SSWC001SSWC001SSWC001001 /* SwitchStatisticsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchStatisticsWindowController.swift; sourceTree = ""; }; TSSC001TSSC001TSSC001001 /* TacticalSidebarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalSidebarSection.swift; sourceTree = ""; }; TTHC001TTHC001TTHC001001 /* TacticalTableHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalTableHeaderCell.swift; sourceTree = ""; }; PCPR001PCPR001PCPR001001 /* PacketCaptureProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketCaptureProtocol.swift; sourceTree = ""; }; @@ -246,6 +250,8 @@ THBT001THBT001THBT001001 /* TacticalHoverButton.swift */, TTRV001TTRV001TTRV001001 /* TacticalTableRowView.swift */, TESV001TESV001TESV001001 /* TacticalEmptyStateView.swift */, + ICMW001ICMW001ICMW001001 /* ISOCacheManagerWindow.swift */, + SSWC001SSWC001SSWC001001 /* SwitchStatisticsWindowController.swift */, TSSC001TSSC001TSSC001001 /* TacticalSidebarSection.swift */, TTHC001TTHC001TTHC001001 /* TacticalTableHeaderCell.swift */, NTNM001NTNM001NTNM001001 /* NotificationNames.swift */, @@ -469,6 +475,8 @@ THBT002THBT002THBT002002 /* TacticalHoverButton.swift in Sources */, TTRV002TTRV002TTRV002002 /* TacticalTableRowView.swift in Sources */, TESV002TESV002TESV002002 /* TacticalEmptyStateView.swift in Sources */, + ICMW002ICMW002ICMW002002 /* ISOCacheManagerWindow.swift in Sources */, + SSWC002SSWC002SSWC002002 /* SwitchStatisticsWindowController.swift in Sources */, TSSC002TSSC002TSSC002002 /* TacticalSidebarSection.swift in Sources */, TTHC002TTHC002TTHC002002 /* TacticalTableHeaderCell.swift in Sources */, NTNM002NTNM002NTNM002002 /* NotificationNames.swift in Sources */, diff --git a/SecVF/AppDelegate.swift b/SecVF/AppDelegate.swift index 60a3ece..f75a5b9 100644 --- a/SecVF/AppDelegate.swift +++ b/SecVF/AppDelegate.swift @@ -43,15 +43,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, VZVirtualMachineDelegate, NS private var attachScriptsUSBFlags: [UUID: Bool] = [:] // Switch statistics window (retained to prevent deallocation) - // TODO: Uncomment when SwitchStatisticsWindowController.swift is added to Xcode project - // private var switchStatisticsWindow: SwitchStatisticsWindowController? + private var switchStatisticsWindow: SwitchStatisticsWindowController? // Packet Analysis window (retained to prevent deallocation) private var packetAnalysisWindow: PacketAnalysisWindowController? // ISO Cache Manager window (retained to prevent deallocation) - // TODO: Add ISOCacheManagerWindow.swift to Xcode project - // private var isoCacheManagerWindow: ISOCacheManagerWindow? + private var isoCacheManagerWindow: ISOCacheManagerWindow? // Splash screen (retained while showing) private var splashScreen: SplashScreenWindow? @@ -1681,36 +1679,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, VZVirtualMachineDelegate, NS } @objc private func showISOCacheManager() { - // ISOCacheManagerWindow.swift exists in the repo but is not yet - // added to the Xcode project target. The menu item was previously - // a silent NSLog — surface an honest alert instead so the user - // isn't left wondering why nothing happened. - // TODO(pre-launch): wire ISOCacheManagerWindow into the Xcode - // project target and replace this alert with the real call site. - let alert = NSAlert() - alert.messageText = "ISO Cache Manager — coming soon" - alert.informativeText = "This feature is in development and not yet enabled in this build. For now, manage cached ISOs directly under ~/.avf/ISOCache/." - alert.alertStyle = .informational - alert.addButton(withTitle: "OK") - alert.runModal() + // ISOCacheManagerWindow is the proper UI for the on-disk + // ~/.avf/ISOCache/ contents: per-image checksum status, + // verify-all, clear-all, last-used dates. Lazy-instantiate + + // retain so the window survives across closes. + if isoCacheManagerWindow == nil || isoCacheManagerWindow?.window == nil { + isoCacheManagerWindow = ISOCacheManagerWindow() + } + isoCacheManagerWindow?.showWindow(nil) + isoCacheManagerWindow?.window?.makeKeyAndOrderFront(nil) } @objc private func showSwitchStatistics() { - // SwitchStatisticsWindowController.swift exists in the repo but - // is not yet added to the Xcode project target. Previously this - // dumped to stdout (invisible). Surface an honest alert + offer - // to print to Console as a fallback. - // TODO(pre-launch): wire SwitchStatisticsWindowController and - // replace this alert with the real call site. - let alert = NSAlert() - alert.messageText = "Switch Statistics window — coming soon" - alert.informativeText = "The in-app statistics window is in development. For now, current stats have been printed to the system log (Console.app, subsystem com.DaxxSec.SecVF)." - alert.alertStyle = .informational - alert.addButton(withTitle: "OK") - alert.runModal() - - // Still emit to OSLog for users who know to look for it. - VirtualNetworkSwitch.shared.printStatistics() + // SwitchStatisticsWindowController owns the in-window view of + // VirtualNetworkSwitch.getStatistics() — per-port packet/byte + // counts plus the global running/MAC-learned/forwarded counters. + // Auto-refreshes every 2s while open. + if switchStatisticsWindow == nil || switchStatisticsWindow?.window == nil { + switchStatisticsWindow = SwitchStatisticsWindowController() + } + switchStatisticsWindow?.showWindow(nil) + switchStatisticsWindow?.window?.makeKeyAndOrderFront(nil) } // MARK: - Tools Menu Handlers diff --git a/SecVF/SwitchStatisticsWindowController.swift b/SecVF/SwitchStatisticsWindowController.swift index 7da9b81..f60ab04 100644 --- a/SecVF/SwitchStatisticsWindowController.swift +++ b/SecVF/SwitchStatisticsWindowController.swift @@ -41,7 +41,13 @@ class SwitchStatisticsWindowController: NSWindowController { } deinit { - stopAutoRefresh() + // Tear down the refresh timer directly — `stopAutoRefresh` is + // main-actor isolated and can't be called from a nonisolated + // deinit. The Timer + nil-out are both safe from any thread + // (Timer.invalidate is documented thread-safe; the optional + // assignment doesn't touch UI state). + refreshTimer?.invalidate() + refreshTimer = nil } // MARK: - UI Setup