Fix USB/IP re-enumeration and export availability

This commit is contained in:
世界 2026-04-21 18:45:40 +08:00
parent 70baa8f354
commit c511144a3d
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
3 changed files with 165 additions and 53 deletions

View File

@ -22,6 +22,18 @@ import (
const clientReconnectDelay = 5 * time.Second
type clientTarget struct {
fixedBusID string
match option.USBIPDeviceMatch
}
func (t clientTarget) description() string {
if t.fixedBusID != "" {
return describeMatch(option.USBIPDeviceMatch{BusID: t.fixedBusID})
}
return describeMatch(t.match)
}
type ClientService struct {
boxService.Adapter
ctx context.Context
@ -31,6 +43,10 @@ type ClientService struct {
serverAddr M.Socksaddr
matches []option.USBIPDeviceMatch // empty = import all remote exports
assignMu sync.Mutex
targets []clientTarget
assigned []string
attachMu sync.Mutex // serializes vhci port pick + attach
wg sync.WaitGroup
@ -97,32 +113,55 @@ func (c *ClientService) Close() error {
return nil
}
// run resolves the desired busid set (once) and spawns a worker per busid.
// run prepares the desired targets and spawns one worker per target.
func (c *ClientService) run() {
defer c.wg.Done()
busids := c.resolveBusIDs()
if len(busids) == 0 {
targets := c.buildTargets()
if len(targets) == 0 {
c.logger.Warn("no devices to import; client idle")
return
}
for _, busid := range busids {
c.assignMu.Lock()
c.targets = targets
c.assigned = make([]string, len(targets))
for i := range targets {
c.assigned[i] = targets[i].fixedBusID
}
c.assignMu.Unlock()
for i := range targets {
c.wg.Add(1)
go c.worker(busid)
go c.worker(i)
}
}
// resolveBusIDs connects once, issues OP_REQ_DEVLIST, and returns the busids
// to attach. For the empty-matches case, returns every remote busid.
// For filtered mode, returns the first match per criterion.
func (c *ClientService) resolveBusIDs() []string {
// Busid-only matches don't require enumeration.
if len(c.matches) > 0 && everyMatchBusIDOnly(c.matches) {
out := make([]string, 0, len(c.matches))
for _, m := range c.matches {
out = append(out, m.BusID)
func (c *ClientService) buildTargets() []clientTarget {
if len(c.matches) == 0 {
busids := c.snapshotRemoteBusIDs()
targets := make([]clientTarget, 0, len(busids))
for _, busid := range busids {
targets = append(targets, clientTarget{fixedBusID: busid})
}
return dedupe(out)
return targets
}
seenFixed := make(map[string]struct{})
targets := make([]clientTarget, 0, len(c.matches))
for _, m := range c.matches {
if isBusIDOnlyMatch(m) {
if _, seen := seenFixed[m.BusID]; seen {
continue
}
seenFixed[m.BusID] = struct{}{}
targets = append(targets, clientTarget{fixedBusID: m.BusID})
continue
}
targets = append(targets, clientTarget{match: m})
}
return targets
}
// snapshotRemoteBusIDs connects once, issues OP_REQ_DEVLIST, and returns the
// currently exported remote busids.
func (c *ClientService) snapshotRemoteBusIDs() []string {
for {
if err := c.ctx.Err(); err != nil {
return nil
@ -135,33 +174,9 @@ func (c *ClientService) resolveBusIDs() []string {
}
continue
}
if len(c.matches) == 0 {
out := make([]string, 0, len(entries))
for i := range entries {
out = append(out, entries[i].Info.BusIDString())
}
return out
}
var out []string
for _, m := range c.matches {
picked := ""
for i := range entries {
key := DeviceKey{
BusID: entries[i].Info.BusIDString(),
VendorID: entries[i].Info.IDVendor,
ProductID: entries[i].Info.IDProduct,
Serial: entries[i].Info.SerialString(),
}
if Matches(m, key) {
picked = key.BusID
break
}
}
if picked == "" {
c.logger.Warn("no remote device matched ", describeMatch(m))
continue
}
out = append(out, picked)
out := make([]string, 0, len(entries))
for i := range entries {
out = append(out, entries[i].Info.BusIDString())
}
return dedupe(out)
}
@ -186,16 +201,32 @@ func (c *ClientService) fetchDevList() ([]DeviceEntry, error) {
return ReadOpRepDevListBody(conn)
}
// worker keeps one remote busid attached to vhci_hcd.0. On any error or
// kernel-side detach, waits clientReconnectDelay and retries.
func (c *ClientService) worker(busid string) {
// worker keeps one target attached to vhci_hcd.0. On any error or kernel-side
// detach, waits clientReconnectDelay and retries.
func (c *ClientService) worker(targetIndex int) {
defer c.wg.Done()
target := c.targets[targetIndex]
for {
if err := c.ctx.Err(); err != nil {
return
}
busid, err := c.claimTargetBusID(targetIndex)
if err != nil {
c.logger.Error("assign ", target.description(), ": ", err)
if !sleepCtx(c.ctx, clientReconnectDelay) {
return
}
continue
}
if busid == "" {
if !sleepCtx(c.ctx, clientReconnectDelay) {
return
}
continue
}
port, err := c.attemptAttach(busid)
if err != nil {
c.releaseTargetBusID(targetIndex, busid)
c.logger.Error("attach ", busid, ": ", err)
if !sleepCtx(c.ctx, clientReconnectDelay) {
return
@ -206,6 +237,7 @@ func (c *ClientService) worker(busid string) {
c.trackPort(port, true)
c.watchPort(port, busid)
c.trackPort(port, false)
c.releaseTargetBusID(targetIndex, busid)
if err := c.ctx.Err(); err != nil {
return
}
@ -216,6 +248,62 @@ func (c *ClientService) worker(busid string) {
}
}
func (c *ClientService) claimTargetBusID(targetIndex int) (string, error) {
target := c.targets[targetIndex]
if target.fixedBusID != "" {
return target.fixedBusID, nil
}
c.assignMu.Lock()
current := c.assigned[targetIndex]
c.assignMu.Unlock()
if current != "" {
return current, nil
}
entries, err := c.fetchDevList()
if err != nil {
return "", err
}
return c.refreshAssignments(targetIndex, entries), nil
}
func (c *ClientService) refreshAssignments(targetIndex int, entries []DeviceEntry) string {
c.assignMu.Lock()
defer c.assignMu.Unlock()
if c.assigned[targetIndex] != "" {
return c.assigned[targetIndex]
}
reserved := make(map[string]struct{}, len(c.assigned))
for _, busid := range c.assigned {
if busid == "" {
continue
}
reserved[busid] = struct{}{}
}
for i, target := range c.targets {
if target.fixedBusID != "" || c.assigned[i] != "" {
continue
}
busid := firstMatchingUnclaimedBusID(target.match, entries, reserved)
if busid == "" {
continue
}
c.assigned[i] = busid
reserved[busid] = struct{}{}
}
return c.assigned[targetIndex]
}
func (c *ClientService) releaseTargetBusID(targetIndex int, busid string) {
if c.targets[targetIndex].fixedBusID != "" {
return
}
c.assignMu.Lock()
defer c.assignMu.Unlock()
if c.assigned[targetIndex] == busid {
c.assigned[targetIndex] = ""
}
}
// attemptAttach performs one dial → OP_REQ_IMPORT → vhci attach sequence.
// The returned TCP socket is handed to the kernel on success; on failure the
// connection is closed before return.
@ -304,13 +392,26 @@ func (c *ClientService) trackPort(port int, add bool) {
}
}
func everyMatchBusIDOnly(matches []option.USBIPDeviceMatch) bool {
for _, m := range matches {
if m.BusID == "" || m.VendorID != 0 || m.ProductID != 0 || m.Serial != "" {
return false
func isBusIDOnlyMatch(m option.USBIPDeviceMatch) bool {
return m.BusID != "" && m.VendorID == 0 && m.ProductID == 0 && m.Serial == ""
}
func firstMatchingUnclaimedBusID(match option.USBIPDeviceMatch, entries []DeviceEntry, reserved map[string]struct{}) string {
for i := range entries {
key := DeviceKey{
BusID: entries[i].Info.BusIDString(),
VendorID: entries[i].Info.IDVendor,
ProductID: entries[i].Info.IDProduct,
Serial: entries[i].Info.SerialString(),
}
if _, claimed := reserved[key.BusID]; claimed {
continue
}
if Matches(match, key) {
return key.BusID
}
}
return true
return ""
}
func dedupe(in []string) []string {

View File

@ -257,6 +257,14 @@ func (s *ServerService) buildDevListEntries() []DeviceEntry {
}
entries := make([]DeviceEntry, 0, len(busids))
for _, busid := range busids {
status, err := readUsbipStatus(busid)
if err != nil {
s.logger.Debug("status ", busid, ": ", err)
continue
}
if status != usbipStatusAvailable {
continue
}
d, err := readSysfsDevice(busid, sysBusDevicePath(busid))
if err != nil {
s.logger.Debug("refresh ", busid, ": ", err)
@ -282,7 +290,7 @@ func (s *ServerService) handleImport(conn net.Conn) {
return
}
status, err := readUsbipStatus(busid)
if err != nil || status != 1 {
if err != nil || status != usbipStatusAvailable {
s.logger.Info("import rejected (busid ", busid, " status=", status, " err=", err, ")")
_ = WriteOpRepImport(conn, OpStatusError, nil)
return

View File

@ -17,6 +17,10 @@ const (
sysBusUSBDevices = "/sys/bus/usb/devices"
sysUsbipHostDriver = "/sys/bus/usb/drivers/usbip-host"
sysVHCIControllerV0 = "/sys/devices/platform/vhci_hcd.0"
usbipStatusAvailable = 1
usbipStatusUsed = 2
usbipStatusError = 3
)
// sysfsDevice captures the subset of USB device attributes needed for
@ -208,7 +212,6 @@ func hostUnbind(busid string) error {
}
// readUsbipStatus returns the usbip_status attribute value for busid.
// 1 = AVAILABLE, 2 = USED, 3 = ERROR.
func readUsbipStatus(busid string) (int, error) {
raw, err := os.ReadFile(filepath.Join(sysBusUSBDevices, busid, "usbip_status"))
if err != nil {
@ -401,7 +404,7 @@ func speedCodeFromString(s string) uint32 {
return SpeedHigh
case "5000":
return SpeedSuper
case "10000":
case "10000", "20000":
return SpeedSuperPlus
default:
return SpeedUnknown