//go:build darwin package diag import ( "encoding/json" "fmt" "os" "os/exec" "strconv" "strings" ) func isPrivileged() bool { return os.Geteuid() == 0 } func collectPlatform(r *Report) { r.System.OSVersion = macOSVersion() r.System.KernelVersion = commandOutput("uname", "-r") collectMacDevices(r) assessMacCapabilities(r) } func macOSVersion() string { name := commandOutput("sw_vers", "-productName") version := commandOutput("sw_vers", "-productVersion") build := commandOutput("sw_vers", "-buildVersion") parts := []string{} for _, p := range []string{name, version, build} { if p != "" { parts = append(parts, p) } } return strings.Join(parts, " ") } func commandOutput(name string, args ...string) string { out, err := exec.Command(name, args...).Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } // system_profiler's JSON output, as much of it as we care about. type spReport struct { Items []spUSBItem `json:"SPUSBDataType"` } type spUSBItem struct { Name string `json:"_name"` VendorID string `json:"vendor_id"` ProductID string `json:"product_id"` Speed string `json:"device_speed"` Manufacturer string `json:"manufacturer"` SerialNumber string `json:"serial_num"` LocationID string `json:"location_id"` Media []spMedia `json:"Media"` Items []spUSBItem `json:"_items"` } type spMedia struct { Name string `json:"_name"` } // collectMacDevices lists USB devices via system_profiler. // // Going through the command rather than IOKit keeps this cgo-free, which is // what lets the tool be cross-compiled from any machine. It is also enough: // this reports what is present, and on macOS nothing can be shared regardless // until there is an IOKit backend. func collectMacDevices(r *Report) { out, err := exec.Command("system_profiler", "-json", "SPUSBDataType").Output() if err != nil { r.note("system_profiler failed: %v", err) return } var report spReport if err := json.Unmarshal(out, &report); err != nil { r.note("could not parse system_profiler output: %v", err) return } for _, item := range report.Items { collectMacItem(r, item) } } // collectMacItem walks the tree; hubs carry their devices in _items. func collectMacItem(r *Report, item spUSBItem) { if item.VendorID != "" { r.Devices = append(r.Devices, DeviceInfo{ BusID: macBusID(item.LocationID), VendorID: normaliseMacID(item.VendorID), ProductID: normaliseMacID(item.ProductID), Name: macDeviceName(item), Speed: item.Speed, Shareable: false, Blocker: "macOS sharing needs an IOKit backend, which does not exist yet", }) } for _, child := range item.Items { collectMacItem(r, child) } } func macDeviceName(item spUSBItem) string { if item.Manufacturer != "" && item.Name != "" && !strings.HasPrefix(item.Name, item.Manufacturer) { return item.Manufacturer + " " + item.Name } return item.Name } // normaliseMacID turns "0x046d (Logitech Inc.)" into "046d". func normaliseMacID(id string) string { id = strings.TrimSpace(id) if i := strings.Index(id, " "); i > 0 { id = id[:i] } id = strings.TrimPrefix(id, "0x") // Pad to four digits so IDs sort and compare like everywhere else. if v, err := strconv.ParseUint(id, 16, 32); err == nil { return fmt.Sprintf("%04x", v) } return id } // macBusID derives an identifier from the location ID, which encodes the // device's position in the port tree and is stable while it stays plugged in. func macBusID(locationID string) string { locationID = strings.TrimSpace(locationID) if i := strings.Index(locationID, " "); i > 0 { locationID = locationID[:i] } return strings.TrimPrefix(locationID, "0x") } func assessMacCapabilities(r *Report) { r.Sharing = Capability{ Available: false, Reason: "no IOKit backend — macOS has no usbdevfs equivalent", Mechanism: "IOKit (not implemented)", } r.Using = Capability{ Available: false, Reason: "no virtual USB host controller — this needs a signed DriverKit driver", Mechanism: "DriverKit (not implemented)", } r.addCheck("macOS sharing", false, fmt.Sprintf("%d USB device(s) found, but none can be shared yet", len(r.Devices)), "none — the relay server runs on macOS, the client's USB side does not") r.note("Docker does not help here: containers share the host kernel, and " + "on macOS Docker runs in a Linux VM that never sees the host's USB hardware. " + "A full VM with USB passthrough (UTM, Parallels, VMware) does work.") }