From cb9ec99e04c99b28483e7972131aca93c188459d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 May 2026 14:06:31 +0800 Subject: [PATCH] usbip: drain linux stub status on finish-import; iterate vhci controllers --- service/usbip/host_linux.go | 90 ++++++++++++-------- service/usbip/linux_interop_test.go | 31 ++----- service/usbip/sysfs_linux.go | 126 +++++++++++++++++++++++----- 3 files changed, 169 insertions(+), 78 deletions(-) diff --git a/service/usbip/host_linux.go b/service/usbip/host_linux.go index c4207c786..6a3af4e46 100644 --- a/service/usbip/host_linux.go +++ b/service/usbip/host_linux.go @@ -28,7 +28,7 @@ func newPlatformExportHost(logger log.ContextLogger, matches []option.USBIPDevic func newPlatformImportHost(logger log.ContextLogger) (ImportHost, error) { return &linuxImportHost{ logger: logger, - ports: make(map[int]struct{}), + ports: make(map[vhciPortKey]struct{}), }, nil } @@ -239,6 +239,7 @@ func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool, if err != nil && !os.IsNotExist(err) && !isMissingUSBDeviceError(err) { h.logger.Debug("release ", busid, " from usbip-host: ", err) } + waitForUsbipStatusCleared(ctx, busid) return false, nil } @@ -459,14 +460,33 @@ func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) ( } type linuxImportHost struct { - logger log.ContextLogger + logger log.ContextLogger + controllers []vhciController portsAccess sync.Mutex - ports map[int]struct{} + ports map[vhciPortKey]struct{} } func (h *linuxImportHost) Start(ctx context.Context) error { - return ensureKernelPath(sysVHCIControllerV0, "vhci-hcd", "vhci_hcd.0") + controllers, err := discoverVHCIControllers() + if err != nil { + return E.Cause(err, "discover vhci controllers") + } + if len(controllers) == 0 { + err = ensureKernelPath(sysVHCIControllerV0, "vhci-hcd", "vhci_hcd.0") + if err != nil { + return err + } + controllers, err = discoverVHCIControllers() + if err != nil { + return E.Cause(err, "discover vhci controllers") + } + if len(controllers) == 0 { + return E.New("no vhci controllers present after loading vhci-hcd") + } + } + h.controllers = controllers + return nil } func (h *linuxImportHost) Close() error { @@ -483,71 +503,75 @@ func (h *linuxImportHost) Attach(ctx context.Context, info DeviceInfoTruncated, mode = "relay" } h.logger.Debug("usbip client handoff ", info.BusIDString(), ": ", mode) - port, attachErr := h.attachOnce(ctx, info, handoff) + ctrl, port, attachErr := h.attachOnce(ctx, info, handoff) if attachErr != nil { _ = handoff.Close() return nil, attachErr } _ = handoff.Start() return &linuxClientSession{ - handoff: handoff, - host: h, - port: port, + handoff: handoff, + host: h, + controller: ctrl, + port: port, }, nil } -func (h *linuxImportHost) attachOnce(ctx context.Context, info DeviceInfoTruncated, handoff *kernelHandoffSession) (int, error) { - triedPorts := make(map[int]struct{}) +func (h *linuxImportHost) attachOnce(ctx context.Context, info DeviceInfoTruncated, handoff *kernelHandoffSession) (vhciController, int, error) { + triedPorts := make(map[vhciPortKey]struct{}) for { - port, err := vhciPickFreePort(info.Speed, triedPorts) + ctrl, port, err := vhciPickFreePort(h.controllers, info.Speed, triedPorts) if err != nil { - return -1, err + return "", -1, err } - if !h.reservePort(port) { - triedPorts[port] = struct{}{} + key := vhciPortKey{controller: ctrl, port: port} + if !h.reservePort(ctrl, port) { + triedPorts[key] = struct{}{} continue } attachLine := fmt.Sprintf("%d %d %d %d", port, int(handoff.file.Fd()), info.DevID(), info.Speed) - err = writeSysfs(filepath.Join(sysVHCIControllerV0, "attach"), attachLine) + err = writeSysfs(filepath.Join(string(ctrl), "attach"), attachLine) if err != nil { - h.releasePort(port) + h.releasePort(ctrl, port) if errors.Is(err, unix.EBUSY) { - triedPorts[port] = struct{}{} + triedPorts[key] = struct{}{} continue } - return -1, E.Cause(err, "vhci attach") + return "", -1, E.Cause(err, "vhci attach") } err = handoff.closeKernelFD() if err != nil { h.logger.Debug("close kernel fd ", info.BusIDString(), ": ", err) } - return port, nil + return ctrl, port, nil } } -func (h *linuxImportHost) reservePort(port int) bool { +func (h *linuxImportHost) reservePort(ctrl vhciController, port int) bool { + key := vhciPortKey{controller: ctrl, port: port} h.portsAccess.Lock() defer h.portsAccess.Unlock() - if _, exists := h.ports[port]; exists { - h.logger.Debug("vhci port ", port, " already reserved locally") + if _, exists := h.ports[key]; exists { + h.logger.Debug(ctrl.name(), " port ", port, " already reserved locally") return false } - h.logger.Debug("reserve vhci port ", port) - h.ports[port] = struct{}{} + h.logger.Debug("reserve ", ctrl.name(), " port ", port) + h.ports[key] = struct{}{} return true } -func (h *linuxImportHost) releasePort(port int) { +func (h *linuxImportHost) releasePort(ctrl vhciController, port int) { h.portsAccess.Lock() defer h.portsAccess.Unlock() - h.logger.Debug("release vhci port ", port) - delete(h.ports, port) + h.logger.Debug("release ", ctrl.name(), " port ", port) + delete(h.ports, vhciPortKey{controller: ctrl, port: port}) } type linuxClientSession struct { - handoff *kernelHandoffSession - host *linuxImportHost - port int + handoff *kernelHandoffSession + host *linuxImportHost + controller vhciController + port int closeOnce sync.Once closeErr error @@ -567,14 +591,14 @@ func (s *linuxClientSession) Start() error { func (s *linuxClientSession) Close() error { s.closeOnce.Do(func() { - detachErr := writeSysfs(filepath.Join(sysVHCIControllerV0, "detach"), strconv.Itoa(s.port)) + detachErr := writeSysfs(filepath.Join(string(s.controller), "detach"), strconv.Itoa(s.port)) closeErr := s.handoff.Close() - s.host.releasePort(s.port) + s.host.releasePort(s.controller, s.port) s.closeErr = E.Errors(detachErr, closeErr) }) return s.closeErr } func (s *linuxClientSession) Description() string { - return fmt.Sprintf("vhci port %d", s.port) + return fmt.Sprintf("%s port %d", s.controller.name(), s.port) } diff --git a/service/usbip/linux_interop_test.go b/service/usbip/linux_interop_test.go index 976af6a3f..816e44fa4 100644 --- a/service/usbip/linux_interop_test.go +++ b/service/usbip/linux_interop_test.go @@ -229,23 +229,15 @@ func waitForUSBIPTeardown(condition func() bool) bool { } func detachUsedVHCIPorts() { - records, err := readVHCIStatus() - if err != nil { - return - } - for _, record := range records { + for _, record := range readAllVHCIStatus() { if record.state == 6 { - _ = writeSysfs(filepath.Join(sysVHCIControllerV0, "detach"), strconv.Itoa(record.port)) + _ = writeSysfs(filepath.Join(string(record.controller), "detach"), strconv.Itoa(record.port)) } } } func allVHCIPortsIdle() bool { - records, err := readVHCIStatus() - if err != nil { - return true - } - for _, record := range records { + for _, record := range readAllVHCIStatus() { if record.state == 6 { return false } @@ -261,11 +253,7 @@ func waitForAllVHCIPortsIdle(t *testing.T) { func waitForVHCIPortIdle(t *testing.T, port int) { t.Helper() require.Eventually(t, func() bool { - records, err := readVHCIStatus() - if err != nil { - return true - } - for _, record := range records { + for _, record := range readAllVHCIStatus() { if record.port == port && record.state == 6 { return false } @@ -639,11 +627,8 @@ func ensureNoNewImportedNode(t *testing.T, pattern string, before map[string]str func usedVHCIPorts(t *testing.T) map[int]struct{} { t.Helper() - records, err := readVHCIStatus() - require.NoError(t, err) - ports := make(map[int]struct{}) - for _, record := range records { + for _, record := range readAllVHCIStatus() { if record.state == 6 { ports[record.port] = struct{}{} } @@ -656,11 +641,7 @@ func waitForNewUsedVHCIPort(t *testing.T, before map[int]struct{}) int { var port int require.Eventually(t, func() bool { - records, err := readVHCIStatus() - if err != nil { - return false - } - for _, record := range records { + for _, record := range readAllVHCIStatus() { if record.state != 6 { continue } diff --git a/service/usbip/sysfs_linux.go b/service/usbip/sysfs_linux.go index 42fad5d64..7a2501a36 100644 --- a/service/usbip/sysfs_linux.go +++ b/service/usbip/sysfs_linux.go @@ -4,12 +4,15 @@ package usbip import ( "bufio" + "context" "fmt" "os" "os/exec" "path/filepath" + "sort" "strconv" "strings" + "time" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/shell" @@ -18,6 +21,7 @@ import ( const ( sysBusUSBDevices = "/sys/bus/usb/devices" sysUsbipHostDriver = "/sys/bus/usb/drivers/usbip-host" + sysVHCIPlatform = "/sys/devices/platform" sysVHCIControllerV0 = "/sys/devices/platform/vhci_hcd.0" usbipStatusAvailable = 1 @@ -25,6 +29,18 @@ const ( usbipStatusError = 3 ) +// vhciController is the canonical sysfs path of a vhci_hcd platform device, +// e.g. /sys/devices/platform/vhci_hcd.0. The kernel module instantiates +// controllers at load time and never adds more at runtime. +type vhciController string + +func (c vhciController) name() string { return filepath.Base(string(c)) } + +type vhciPortKey struct { + controller vhciController + port int +} + type sysfsDevice struct { BusID string Path string @@ -64,9 +80,10 @@ func (d *sysfsDevice) toProtocol() DeviceInfoTruncated { } type vhciStatusRecord struct { - hub string - port int - state int + controller vhciController + hub string + port int + state int } func listUSBDevices() ([]sysfsDevice, error) { @@ -172,37 +189,105 @@ func readUsbipStatus(busid string) (int, error) { return v, nil } -func vhciPickFreePort(speed uint32, skip map[int]struct{}) (int, error) { - records, err := readVHCIStatus() - if err != nil { - return -1, err +// finishImportStatusTimeout is the upper bound for waitForUsbipStatusCleared. +// It is a var (not const) so interop tests can shrink it without changing the +// polling cadence. +var finishImportStatusTimeout = 2 * time.Second + +const finishImportStatusPollInterval = 25 * time.Millisecond + +// waitForUsbipStatusCleared blocks until usbip_status leaves the "used" +// state, the device disappears, the bounded timeout fires, or ctx is +// cancelled. Writing -1 to usbip_sockfd only schedules the kernel-side down +// event; without this wait the broadcast that follows ReleaseImport would +// re-read the still-"used" status and emit no delta, leaving subscribers +// stuck on the busy view. +func waitForUsbipStatusCleared(ctx context.Context, busid string) { + deadline := time.Now().Add(finishImportStatusTimeout) + for { + status, err := readUsbipStatus(busid) + if err != nil || status != usbipStatusUsed { + return + } + if !time.Now().Before(deadline) { + return + } + if !sleepCtx(ctx, finishImportStatusPollInterval) { + return + } } +} + +func discoverVHCIControllers() ([]vhciController, error) { + matches, err := filepath.Glob(filepath.Join(sysVHCIPlatform, "vhci_hcd.*")) + if err != nil { + return nil, err + } + sort.Strings(matches) + out := make([]vhciController, 0, len(matches)) + for _, path := range matches { + out = append(out, vhciController(path)) + } + return out, nil +} + +func vhciPickFreePort(controllers []vhciController, speed uint32, skip map[vhciPortKey]struct{}) (vhciController, int, error) { targetHub := "hs" switch speed { case SpeedSuper, SpeedSuperPlus: targetHub = "ss" } - for _, record := range records { - if record.hub != targetHub || record.state != 4 { + var firstErr error + for _, ctrl := range controllers { + records, err := readVHCIStatus(ctrl) + if err != nil { + if firstErr == nil { + firstErr = err + } continue } - if _, skipped := skip[record.port]; skipped { - continue + for _, record := range records { + if record.hub != targetHub || record.state != 4 { + continue + } + key := vhciPortKey{controller: ctrl, port: record.port} + if _, skipped := skip[key]; skipped { + continue + } + return ctrl, record.port, nil } - return record.port, nil } - return -1, E.New("no free ", targetHub, " vhci port") + if firstErr != nil { + return "", -1, firstErr + } + return "", -1, E.New("no free ", targetHub, " vhci port") } -func readVHCIStatus() ([]vhciStatusRecord, error) { - raw, err := os.ReadFile(filepath.Join(sysVHCIControllerV0, "status")) +func readVHCIStatus(ctrl vhciController) ([]vhciStatusRecord, error) { + raw, err := os.ReadFile(filepath.Join(string(ctrl), "status")) if err != nil { return nil, err } - return parseVHCIStatus(string(raw)), nil + return parseVHCIStatus(ctrl, string(raw)), nil } -func parseVHCIStatus(raw string) []vhciStatusRecord { +func readAllVHCIStatus() []vhciStatusRecord { + controllers, err := discoverVHCIControllers() + if err != nil { + return nil + } + var out []vhciStatusRecord + for _, ctrl := range controllers { + records, err := readVHCIStatus(ctrl) + if err != nil { + continue + } + out = append(out, records...) + } + return out +} + +func parseVHCIStatus(ctrl vhciController, raw string) []vhciStatusRecord { scanner := bufio.NewScanner(strings.NewReader(raw)) records := make([]vhciStatusRecord, 0) first := true @@ -228,9 +313,10 @@ func parseVHCIStatus(raw string) []vhciStatusRecord { continue } records = append(records, vhciStatusRecord{ - hub: fields[0], - port: port, - state: state, + controller: ctrl, + hub: fields[0], + port: port, + state: state, }) } return records