// Package diag collects everything needed to work out why USB sharing does // not work on a given machine. // // It exists because the failure modes are platform specific and mostly // invisible from the outside: a missing kernel module, a driver that did not // load, permissions on a device node, a filter that is installed but not // attached. Guessing at those across a chat is slow; a structured report // turns it into a matter of reading. package diag import ( "encoding/json" "fmt" "os" "runtime" "strings" "time" ) // Report is the whole diagnostic picture of one machine. type Report struct { // Generated is filled in by the caller, since a report is often written // and read at very different times. Generated string `json:"generated"` Tool ToolInfo `json:"tool"` System SystemInfo `json:"system"` Sharing Capability `json:"sharing"` Using Capability `json:"using"` Devices []DeviceInfo `json:"devices"` Checks []Check `json:"checks"` // Notes carries anything that did not fit elsewhere, in plain language. Notes []string `json:"notes,omitempty"` } // ToolInfo identifies the build that produced the report. type ToolInfo struct { Version string `json:"version"` GoVersion string `json:"go_version"` OS string `json:"os"` Arch string `json:"arch"` } // SystemInfo describes the machine. type SystemInfo struct { Hostname string `json:"hostname"` OSVersion string `json:"os_version,omitempty"` KernelVersion string `json:"kernel_version,omitempty"` Privileged bool `json:"privileged"` // Container reports whether we appear to be inside one, which changes // what device access means. Container bool `json:"container,omitempty"` } // Capability reports whether one half of the system can work here. type Capability struct { Available bool `json:"available"` Reason string `json:"reason,omitempty"` // Mechanism names what would be used: "usbdevfs", "usbshare filter", // "vhci-hcd", "usbip-win2". Mechanism string `json:"mechanism,omitempty"` } // DeviceInfo is one USB device as the machine sees it. type DeviceInfo struct { BusID string `json:"bus_id,omitempty"` VendorID string `json:"vendor_id"` ProductID string `json:"product_id"` Name string `json:"name,omitempty"` Class string `json:"class,omitempty"` Driver string `json:"driver,omitempty"` Speed string `json:"speed,omitempty"` // Shareable reports whether this device could actually be offered, and // Blocker says what stands in the way when it cannot. Shareable bool `json:"shareable"` Blocker string `json:"blocker,omitempty"` // Endpoints matter for diagnosing devices that attach but stay silent: // a wrong transfer type here is exactly that symptom. Endpoints []EndpointInfo `json:"endpoints,omitempty"` } // EndpointInfo is one endpoint of a device. type EndpointInfo struct { Address string `json:"address"` Direction string `json:"direction"` TransferType string `json:"transfer_type"` MaxPacket uint16 `json:"max_packet"` Interval uint8 `json:"interval"` } // Check is one named test with a verdict. type Check struct { Name string `json:"name"` Passed bool `json:"passed"` Detail string `json:"detail,omitempty"` // Fix is a concrete action, present only when the check failed and there // is something the user can actually do. Fix string `json:"fix,omitempty"` } // Collect gathers a report for the current machine. func Collect(version string) *Report { hostname, _ := os.Hostname() report := &Report{ Generated: time.Now().Format(time.RFC3339), Tool: ToolInfo{ Version: version, GoVersion: runtime.Version(), OS: runtime.GOOS, Arch: runtime.GOARCH, }, System: SystemInfo{ Hostname: hostname, Privileged: isPrivileged(), Container: inContainer(), }, } collectPlatform(report) return report } // JSON renders the report for machine consumption. func (r *Report) JSON() ([]byte, error) { return json.MarshalIndent(r, "", " ") } // String renders the report for a human reading a terminal. func (r *Report) String() string { var b strings.Builder fmt.Fprintf(&b, "USB Server diagnostics\n") fmt.Fprintf(&b, "======================\n\n") fmt.Fprintf(&b, "Host: %s (%s/%s)\n", r.System.Hostname, r.Tool.OS, r.Tool.Arch) if r.System.OSVersion != "" { fmt.Fprintf(&b, "OS: %s\n", r.System.OSVersion) } if r.System.KernelVersion != "" { fmt.Fprintf(&b, "Kernel: %s\n", r.System.KernelVersion) } fmt.Fprintf(&b, "Elevated: %v\n", r.System.Privileged) if r.System.Container { fmt.Fprintf(&b, "Container: yes\n") } fmt.Fprintf(&b, "\n") fmt.Fprintf(&b, "Sharing devices: %s\n", capabilityLine(r.Sharing)) fmt.Fprintf(&b, "Using devices: %s\n", capabilityLine(r.Using)) fmt.Fprintf(&b, "\n") if len(r.Checks) > 0 { fmt.Fprintf(&b, "Checks\n------\n") for _, c := range r.Checks { mark := "FAIL" if c.Passed { mark = " ok " } fmt.Fprintf(&b, "[%s] %s\n", mark, c.Name) if c.Detail != "" { fmt.Fprintf(&b, " %s\n", c.Detail) } if !c.Passed && c.Fix != "" { fmt.Fprintf(&b, " fix: %s\n", c.Fix) } } fmt.Fprintf(&b, "\n") } fmt.Fprintf(&b, "Devices (%d)\n-----------\n", len(r.Devices)) for _, d := range r.Devices { state := "shareable" if !d.Shareable { state = "blocked: " + d.Blocker } fmt.Fprintf(&b, "%-12s %s:%s %-28s %s\n", d.BusID, d.VendorID, d.ProductID, truncate(d.Name, 28), state) if d.Driver != "" { fmt.Fprintf(&b, " driver=%s class=%s speed=%s\n", d.Driver, d.Class, d.Speed) } for _, ep := range d.Endpoints { fmt.Fprintf(&b, " ep %s %-3s %-11s maxpkt=%d interval=%d\n", ep.Address, ep.Direction, ep.TransferType, ep.MaxPacket, ep.Interval) } } if len(r.Notes) > 0 { fmt.Fprintf(&b, "\nNotes\n-----\n") for _, n := range r.Notes { fmt.Fprintf(&b, "- %s\n", n) } } return b.String() } func capabilityLine(c Capability) string { if c.Available { if c.Mechanism != "" { return "yes (" + c.Mechanism + ")" } return "yes" } if c.Reason != "" { return "no — " + c.Reason } return "no" } func truncate(s string, max int) string { if len(s) <= max { return s } if max <= 3 { return s[:max] } return s[:max-3] + "..." } // addCheck appends a check result. func (r *Report) addCheck(name string, passed bool, detail, fix string) { r.Checks = append(r.Checks, Check{ Name: name, Passed: passed, Detail: detail, Fix: fix, }) } // note appends a free-form observation. func (r *Report) note(format string, args ...interface{}) { r.Notes = append(r.Notes, fmt.Sprintf(format, args...)) } // inContainer guesses whether this process runs inside a container. // // It matters for diagnosis: inside a container, missing devices usually mean // the container was not given access, not that the host lacks them. func inContainer() bool { if _, err := os.Stat("/.dockerenv"); err == nil { return true } if data, err := os.ReadFile("/proc/1/cgroup"); err == nil { content := string(data) if strings.Contains(content, "docker") || strings.Contains(content, "containerd") || strings.Contains(content, "lxc") { return true } } return false }