← back to Exo
Display RDMA debug info in macOS app. (#1072)
ca680185f3b1ade917626b13c19e67982ad0bca3 · 2026-01-08 15:17:00 +0000 · Alex Cheema
## Motivation
Often users are running into issues with RDMA. See
https://github.com/exo-explore/exo/issues?q=is%3Aissue%20rdma
Having some debug info in the macOS app will help to debug these issues.
## Changes
Displays output of the following commands in the debug info section of
the macOS app:
1. `rdma_ctl status`
2. `ibv_devices`
3. `ibv_devinfo`
## Why It Works
It displays RDMA debug info in the debug info section of the macOS app.
## Test Plan
### Manual Testing
We need to make a new build of the macOS app and check the output under
the following conditions:
1. No RDMA enabled.
2. RDMA enabled but no devices connected over TB5.
3. RDMA enabled and devices connected over TB5.
Files touched
M app/EXO/EXO/ContentView.swiftM app/EXO/EXO/Services/BugReportService.swiftM app/EXO/EXO/Services/NetworkStatusService.swift
Diff
commit ca680185f3b1ade917626b13c19e67982ad0bca3
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Thu Jan 8 15:17:00 2026 +0000
Display RDMA debug info in macOS app. (#1072)
## Motivation
Often users are running into issues with RDMA. See
https://github.com/exo-explore/exo/issues?q=is%3Aissue%20rdma
Having some debug info in the macOS app will help to debug these issues.
## Changes
Displays output of the following commands in the debug info section of
the macOS app:
1. `rdma_ctl status`
2. `ibv_devices`
3. `ibv_devinfo`
## Why It Works
It displays RDMA debug info in the debug info section of the macOS app.
## Test Plan
### Manual Testing
We need to make a new build of the macOS app and check the output under
the following conditions:
1. No RDMA enabled.
2. RDMA enabled but no devices connected over TB5.
3. RDMA enabled and devices connected over TB5.
---
app/EXO/EXO/ContentView.swift | 47 +++++++++++++
app/EXO/EXO/Services/BugReportService.swift | 27 ++++++-
app/EXO/EXO/Services/NetworkStatusService.swift | 93 ++++++++++++++++++++++++-
3 files changed, 164 insertions(+), 3 deletions(-)
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 {
← 383309e2 fmt: add typescript formatting
·
back to Exo
·
Use presigned URLs for bug report uploads (#1109) 4759b09d →