From 534e8d565eeacf2c3ed64bda7f63aad2c96c9520 Mon Sep 17 00:00:00 2001 From: DaxxSec Date: Wed, 13 May 2026 10:32:10 -0600 Subject: [PATCH] feat: wire ISOCacheManagerWindow + SwitchStatisticsWindowController to Xcode target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both source files have existed in the repo for a while but were never added to the Xcode project, so the two "Tools" menu items that referenced them fell back to "coming soon" alerts. This PR pulls them into the build target and replaces the stub alerts with real window instantiation. What lights up ============== - **Tools → ISO Cache Manager**: opens `ISOCacheManagerWindow`. Lists every cached ISO/IPSW under `~/.avf/ISOCache/` with per-image checksum status (verified / not verified / placeholder / failed), size, last-used date, search/filter, and Clear All. The window was already written, so this is pure wire-up — no UI code change. - **Tools → Virtual Switch Statistics**: opens `SwitchStatisticsWindowController`. Auto-refresh every 2s of the global packetsForwarded/Broadcast/MACs counters + per-port packets-rx/tx + bytes-rx/tx. Was previously dumping the same data silently to OSLog via `VirtualNetworkSwitch.printStatistics()`, which made the menu item effectively invisible. Both windows lazy-instantiate on first menu click + retain via an ivar across closes, matching the existing pattern for PacketAnalysisWindow and the three LogViewerWindowController instances. Build fix ========= `SwitchStatisticsWindowController.deinit` called the main-actor- isolated `stopAutoRefresh()` method, which Xcode's stricter Swift 6 concurrency checking rejects from a nonisolated deinit. Replaced the call with the direct Timer invalidate + nil-out (Timer.invalidate is documented thread-safe). Documented inline. Full test suite: 278/278 passing (one pre-existing flaky network test skipped as before). Co-Authored-By: Claude Opus 4.7 (1M context) --- SecVF.xcodeproj/project.pbxproj | 8 +++ SecVF/AppDelegate.swift | 51 ++++++++------------ SecVF/SwitchStatisticsWindowController.swift | 8 ++- 3 files changed, 35 insertions(+), 32 deletions(-) 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