From 0e34a2d088ea89147a40c411fbb9637dec367ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Apr 2026 22:48:14 +0800 Subject: [PATCH] Fix usbip host bind recovery --- service/usbip/linux_test.go | 69 ++++++++++++++++++++++++++++++++++- service/usbip/ops_linux.go | 2 + service/usbip/server_linux.go | 28 +++++++++++++- service/usbip/sysfs_linux.go | 12 ++++++ 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/service/usbip/linux_test.go b/service/usbip/linux_test.go index 3e1296cb9..520673df7 100644 --- a/service/usbip/linux_test.go +++ b/service/usbip/linux_test.go @@ -201,6 +201,10 @@ func newTestUSBIPOps(t *testing.T) usbipOps { t.Fatalf("unexpected hostUnbind") return nil }, + reloadHostDriver: func() error { + t.Fatalf("unexpected reloadHostDriver") + return nil + }, readUsbipStatus: func(string) (int, error) { t.Fatalf("unexpected readUsbipStatus") return 0, nil @@ -837,6 +841,70 @@ func TestServerReconcileExportsBindsMatchesAndSkipsHub(t *testing.T) { }, server.snapshotExports()) } +func TestServerBindOneRetriesAfterStaleHostMatch(t *testing.T) { + t.Parallel() + + device := newTestDevice("1-1", 0x1d6b, 0x0104, "regular", SpeedHigh) + ops := newTestUSBIPOps(t) + var actions []string + bindCalls := 0 + ops.currentDriver = func(busid string) (string, error) { + return "usb", nil + } + ops.unbindFromDriver = func(busid, driver string) error { + actions = append(actions, "unbind "+busid+" "+driver) + return nil + } + ops.hostMatchBusID = func(busid string, add bool) error { + actions = append(actions, "match "+busid+" "+map[bool]string{true: "add", false: "del"}[add]) + return nil + } + ops.hostBind = func(busid string) error { + bindCalls++ + actions = append(actions, "hostbind "+busid) + if bindCalls == 1 { + return &os.PathError{Op: "write", Path: filepath.Join(sysUsbipHostDriver, "bind"), Err: unix.ENODEV} + } + return nil + } + ops.bindToDriver = func(busid, driver string) error { + actions = append(actions, "bind "+busid+" "+driver) + return nil + } + ops.reloadHostDriver = func() error { + actions = append(actions, "reload") + return nil + } + + server := &ServerService{ + ctx: context.Background(), + logger: newTestLogger(), + exports: make(map[string]serverExport), + controlSubs: make(map[uint64]*serverControlConn), + ops: ops, + } + + require.NoError(t, server.bindOne(&device)) + require.Equal(t, []string{ + "unbind 1-1 usb", + "match 1-1 add", + "hostbind 1-1", + "match 1-1 del", + "bind 1-1 usb", + "reload", + "unbind 1-1 usb", + "match 1-1 add", + "hostbind 1-1", + }, actions) + require.Equal(t, map[string]serverExport{ + "1-1": { + busid: "1-1", + managed: true, + originalDriver: "usb", + }, + }, server.snapshotExports()) +} + func TestServerReconcileExportsSkipsVHCIDevices(t *testing.T) { t.Parallel() @@ -1047,7 +1115,6 @@ func TestServerCloseSerializesRollbackWithActiveReconcile(t *testing.T) { record("hostunbind " + busid) return nil } - server := &ServerService{ ctx: ctx, cancel: cancel, diff --git a/service/usbip/ops_linux.go b/service/usbip/ops_linux.go index 1c9093f39..ca8506294 100644 --- a/service/usbip/ops_linux.go +++ b/service/usbip/ops_linux.go @@ -19,6 +19,7 @@ type usbipOps struct { hostMatchBusID func(busid string, add bool) error hostBind func(busid string) error hostUnbind func(busid string) error + reloadHostDriver func() error readUsbipStatus func(busid string) (int, error) writeUsbipSockfd func(busid string, fd int) error newUEventListener func() (usbEventListener, error) @@ -40,6 +41,7 @@ var systemUSBIPOps = usbipOps{ hostMatchBusID: hostMatchBusID, hostBind: hostBind, hostUnbind: hostUnbind, + reloadHostDriver: reloadHostDriver, readUsbipStatus: readUsbipStatus, writeUsbipSockfd: writeUsbipSockfd, newUEventListener: func() (usbEventListener, error) { diff --git a/service/usbip/server_linux.go b/service/usbip/server_linux.go index e5bffd496..c6346a615 100644 --- a/service/usbip/server_linux.go +++ b/service/usbip/server_linux.go @@ -173,8 +173,7 @@ func (s *ServerService) reconcileExports() (bool, error) { continue } if err := s.bindOne(&device); err != nil { - s.logger.Warn("bind ", busid, ": ", err) - continue + return changed, E.Cause(err, "bind ", busid) } changed = true } @@ -192,6 +191,24 @@ func (s *ServerService) reconcileExports() (bool, error) { } func (s *ServerService) bindOne(d *sysfsDevice) error { + var err error + for attempt := 0; attempt < 2; attempt++ { + err = s.bindOneOnce(d) + if err == nil { + return nil + } + if attempt > 0 || !errors.Is(err, unix.ENODEV) { + break + } + s.logger.Warn("reset usbip-host after bind failure on ", d.BusID, ": ", err) + if resetErr := s.resetHostDriverForBindRetry(); resetErr != nil { + return E.Cause(resetErr, "reset usbip-host after bind failure") + } + } + return err +} + +func (s *ServerService) bindOneOnce(d *sysfsDevice) error { driver, err := s.ops.currentDriver(d.BusID) if err != nil { return err @@ -228,6 +245,13 @@ func (s *ServerService) bindOne(d *sysfsDevice) error { return nil } +func (s *ServerService) resetHostDriverForBindRetry() error { + if len(s.snapshotExports()) > 0 { + return E.New("active usbip-host exports are present") + } + return s.ops.reloadHostDriver() +} + func (s *ServerService) releaseExport(export serverExport, restore bool) error { if !export.managed { s.deleteExport(export.busid) diff --git a/service/usbip/sysfs_linux.go b/service/usbip/sysfs_linux.go index 4e43749ce..1c51ce9ac 100644 --- a/service/usbip/sysfs_linux.go +++ b/service/usbip/sysfs_linux.go @@ -215,6 +215,18 @@ func hostUnbind(busid string) error { return writeSysfs(filepath.Join(sysUsbipHostDriver, "unbind"), busid) } +func reloadHostDriver() error { + modprobePath, err := findModprobePath() + if err != nil { + return err + } + output, err := shell.Exec(modprobePath, "-r", "usbip-host").Read() + if err != nil { + return E.Extend(E.Cause(err, "unload kernel module usbip-host"), strings.TrimSpace(output)) + } + return ensureHostDriver() +} + // readUsbipStatus returns the usbip_status attribute value for busid. func readUsbipStatus(busid string) (int, error) { raw, err := os.ReadFile(filepath.Join(sysBusUSBDevices, busid, "usbip_status"))