diff --git a/app/EXO/EXO/ContentView.swift b/app/EXO/EXO/ContentView.swift index 4107a938..d9904c91 100644 --- a/app/EXO/EXO/ContentView.swift +++ b/app/EXO/EXO/ContentView.swift @@ -459,6 +459,7 @@ struct ContentView: View { .font(.caption2) .foregroundColor(thunderboltStatusColor) interfaceIpList + rdmaStatusView sendBugReportButton .padding(.top, 6) } @@ -468,6 +469,52 @@ struct ContentView: View { .animation(.easeInOut(duration: 0.25), value: showDebugInfo) } + private var rdmaStatusView: some View { + let rdma = networkStatusService.status.rdmaStatus + return VStack(alignment: .leading, spacing: 1) { + Text("RDMA: \(rdmaStatusText(rdma))") + .font(.caption2) + .foregroundColor(rdmaStatusColor(rdma)) + if !rdma.devices.isEmpty { + Text(" Devices: \(rdma.devices.joined(separator: ", "))") + .font(.caption2) + .foregroundColor(.secondary) + } + if !rdma.activePorts.isEmpty { + Text(" Active Ports:") + .font(.caption2) + .foregroundColor(.secondary) + ForEach(rdma.activePorts, id: \.device) { port in + Text(" \(port.device) port \(port.port): \(port.state)") + .font(.caption2) + .foregroundColor(.green) + } + } + } + } + + private func rdmaStatusText(_ rdma: RDMAStatus) -> String { + switch rdma.rdmaCtlEnabled { + case .some(true): + return "Enabled" + case .some(false): + return "Disabled" + case nil: + return rdma.devices.isEmpty ? "Not Available" : "Available" + } + } + + private func rdmaStatusColor(_ rdma: RDMAStatus) -> Color { + switch rdma.rdmaCtlEnabled { + case .some(true): + return .green + case .some(false): + return .orange + case nil: + return rdma.devices.isEmpty ? .secondary : .green + } + } + private var sendBugReportButton: some View { VStack(alignment: .leading, spacing: 4) { Button { diff --git a/app/EXO/EXO/Services/BugReportService.swift b/app/EXO/EXO/Services/BugReportService.swift index e1ee9031..4cd4639d 100644 --- a/app/EXO/EXO/Services/BugReportService.swift +++ b/app/EXO/EXO/Services/BugReportService.swift @@ -110,7 +110,16 @@ struct BugReportService { private func readDebugInfo() -> DebugInfo { DebugInfo( thunderboltBridgeDisabled: readThunderboltBridgeDisabled(), - interfaces: readInterfaces() + interfaces: readInterfaces(), + rdma: readRDMADebugInfo() + ) + } + + private func readRDMADebugInfo() -> DebugInfo.RDMADebugInfo { + DebugInfo.RDMADebugInfo( + rdmaCtlStatus: safeRunCommand(["/usr/bin/rdma_ctl", "status"]), + ibvDevices: safeRunCommand(["/usr/bin/ibv_devices"]), + ibvDevinfo: safeRunCommand(["/usr/bin/ibv_devinfo"]) ) } @@ -360,6 +369,7 @@ struct BugReportService { private struct DebugInfo { let thunderboltBridgeDisabled: Bool? let interfaces: [InterfaceStatus] + let rdma: RDMADebugInfo struct InterfaceStatus { let name: String @@ -373,10 +383,25 @@ private struct DebugInfo { } } + struct RDMADebugInfo { + let rdmaCtlStatus: String? + let ibvDevices: String? + let ibvDevinfo: String? + + func toDictionary() -> [String: Any] { + [ + "rdma_ctl_status": rdmaCtlStatus as Any, + "ibv_devices": ibvDevices as Any, + "ibv_devinfo": ibvDevinfo as Any, + ] + } + } + func toDictionary() -> [String: Any] { [ "thunderbolt_bridge_disabled": thunderboltBridgeDisabled as Any, "interfaces": interfaces.map { $0.toDictionary() }, + "rdma": rdma.toDictionary(), ] } } diff --git a/app/EXO/EXO/Services/NetworkStatusService.swift b/app/EXO/EXO/Services/NetworkStatusService.swift index 1c390121..dea77a1f 100644 --- a/app/EXO/EXO/Services/NetworkStatusService.swift +++ b/app/EXO/EXO/Services/NetworkStatusService.swift @@ -35,14 +35,34 @@ struct NetworkStatus: Equatable { let thunderboltBridgeState: ThunderboltState? let bridgeInactive: Bool? let interfaceStatuses: [InterfaceIpStatus] + let rdmaStatus: RDMAStatus static let empty = NetworkStatus( thunderboltBridgeState: nil, bridgeInactive: nil, - interfaceStatuses: [] + interfaceStatuses: [], + rdmaStatus: .empty ) } +struct RDMAStatus: Equatable { + let rdmaCtlEnabled: Bool? + let devices: [String] + let activePorts: [RDMAPort] + + var isAvailable: Bool { + rdmaCtlEnabled == true || !devices.isEmpty + } + + static let empty = RDMAStatus(rdmaCtlEnabled: nil, devices: [], activePorts: []) +} + +struct RDMAPort: Equatable { + let device: String + let port: String + let state: String +} + struct InterfaceIpStatus: Equatable { let interfaceName: String let ipAddress: String? @@ -59,10 +79,79 @@ private struct NetworkStatusFetcher { NetworkStatus( thunderboltBridgeState: readThunderboltBridgeState(), bridgeInactive: readBridgeInactive(), - interfaceStatuses: readInterfaceStatuses() + interfaceStatuses: readInterfaceStatuses(), + rdmaStatus: readRDMAStatus() ) } + private func readRDMAStatus() -> RDMAStatus { + let rdmaCtlEnabled = readRDMACtlEnabled() + let devices = readRDMADevices() + let activePorts = readRDMAActivePorts() + return RDMAStatus( + rdmaCtlEnabled: rdmaCtlEnabled, devices: devices, activePorts: activePorts) + } + + private func readRDMACtlEnabled() -> Bool? { + let result = runCommand(["rdma_ctl", "status"]) + guard result.exitCode == 0 else { return nil } + let output = result.output.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + if output.contains("enabled") { + return true + } + if output.contains("disabled") { + return false + } + return nil + } + + private func readRDMADevices() -> [String] { + let result = runCommand(["ibv_devices"]) + guard result.exitCode == 0 else { return [] } + var devices: [String] = [] + for line in result.output.split(separator: "\n") { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.hasPrefix("---") || trimmed.lowercased().hasPrefix("device") + || trimmed.isEmpty + { + continue + } + let parts = trimmed.split(separator: " ", maxSplits: 1) + if let deviceName = parts.first { + devices.append(String(deviceName)) + } + } + return devices + } + + private func readRDMAActivePorts() -> [RDMAPort] { + let result = runCommand(["ibv_devinfo"]) + guard result.exitCode == 0 else { return [] } + var ports: [RDMAPort] = [] + var currentDevice: String? + var currentPort: String? + + for line in result.output.split(separator: "\n") { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.hasPrefix("hca_id:") { + currentDevice = trimmed.replacingOccurrences(of: "hca_id:", with: "") + .trimmingCharacters(in: .whitespaces) + } else if trimmed.hasPrefix("port:") { + currentPort = trimmed.replacingOccurrences(of: "port:", with: "") + .trimmingCharacters(in: .whitespaces) + } else if trimmed.hasPrefix("state:") { + let state = trimmed.replacingOccurrences(of: "state:", with: "").trimmingCharacters( + in: .whitespaces) + if let device = currentDevice, let port = currentPort { + if state.lowercased().contains("active") { + ports.append(RDMAPort(device: device, port: port, state: state)) + } + } + } + } + return ports + } + private func readThunderboltBridgeState() -> ThunderboltState? { let result = runCommand(["networksetup", "-getnetworkserviceenabled", "Thunderbolt Bridge"]) guard result.exitCode == 0 else {