usb-server/internal/usbip/vhci.go

178 lines
4.1 KiB
Go

//go:build linux
package usbip
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
const vhciBasePath = "/sys/devices/platform/vhci_hcd.0"
// VHCIPort represents a virtual USB port on the VHCI controller
type VHCIPort struct {
Hub string // "hs" or "ss"
Port int
Status int
Speed int
DevID uint32
SocketFD int
LocalBusID string
}
// VHCI status constants
const (
VDevStNull = 0x04
VDevStNotAssigned = 0x05
VDevStUsed = 0x06
VDevStError = 0x07
)
// ReadVHCIStatus reads the current VHCI port status
func ReadVHCIStatus() ([]VHCIPort, error) {
// Try status file directly, then status.0, status.1, etc.
var allPorts []VHCIPort
paths := []string{
filepath.Join(vhciBasePath, "status"),
}
// Check for multi-controller status files
for i := 0; i < 16; i++ {
p := filepath.Join(vhciBasePath, fmt.Sprintf("status.%d", i))
if _, err := os.Stat(p); err == nil {
paths = append(paths, p)
} else {
break
}
}
for _, path := range paths {
ports, err := parseStatusFile(path)
if err != nil {
continue
}
allPorts = append(allPorts, ports...)
}
if len(allPorts) == 0 {
return nil, fmt.Errorf("vhci-hcd module not loaded or no ports found (check: modprobe vhci-hcd)")
}
return allPorts, nil
}
func parseStatusFile(path string) ([]VHCIPort, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
var ports []VHCIPort
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip header lines
if strings.HasPrefix(line, "hub") || strings.HasPrefix(line, "prt") || line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 7 {
continue
}
port := VHCIPort{Hub: fields[0]}
if v, err := strconv.Atoi(fields[1]); err == nil {
port.Port = v
}
if v, err := strconv.Atoi(fields[2]); err == nil {
port.Status = v
}
if v, err := strconv.Atoi(fields[3]); err == nil {
port.Speed = v
}
if v, err := strconv.ParseUint(fields[4], 16, 32); err == nil {
port.DevID = uint32(v)
}
if v, err := strconv.Atoi(fields[5]); err == nil {
port.SocketFD = v
}
port.LocalBusID = fields[6]
ports = append(ports, port)
}
return ports, nil
}
// FindFreePort finds an available VHCI port for the given speed
func FindFreePort(speed uint32) (int, error) {
ports, err := ReadVHCIStatus()
if err != nil {
return -1, err
}
// Determine desired hub type based on speed
wantHub := "hs" // high-speed and below
if speed >= SpeedSuper {
wantHub = "ss" // super-speed
}
for _, port := range ports {
if port.Status == VDevStNull && port.Hub == wantHub {
return port.Port, nil
}
}
return -1, fmt.Errorf("no free VHCI port available for hub type %s", wantHub)
}
// AttachDevice writes to the VHCI attach file to create a virtual USB device.
// sockfd must be a valid TCP socket file descriptor connected to the USB/IP server.
func AttachDevice(port int, sockfd int, devID uint32, speed uint32) error {
attachPath := filepath.Join(vhciBasePath, "attach")
// Format: "<port> <sockfd> <devid> <speed>"
data := fmt.Sprintf("%d %d %d %d", port, sockfd, devID, speed)
if err := os.WriteFile(attachPath, []byte(data), 0); err != nil {
return fmt.Errorf("writing to VHCI attach: %w", err)
}
return nil
}
// DetachDevice writes to the VHCI detach file to remove a virtual USB device
func DetachDevice(port int) error {
detachPath := filepath.Join(vhciBasePath, "detach")
data := fmt.Sprintf("%d", port)
if err := os.WriteFile(detachPath, []byte(data), 0); err != nil {
return fmt.Errorf("writing to VHCI detach: %w", err)
}
return nil
}
// IsVHCIAvailable checks if the vhci-hcd kernel module is loaded
func IsVHCIAvailable() bool {
_, err := os.Stat(vhciBasePath)
return err == nil
}
// VHCIUnavailableError returns an error describing why VHCI is not available,
// or nil if VHCI is ready to use.
func VHCIUnavailableError() error {
if IsVHCIAvailable() {
return nil
}
return fmt.Errorf("vhci-hcd Kernel-Modul nicht geladen (ausfuehren: sudo modprobe vhci-hcd)")
}