From d30035ccb022599901d84b30cf31837c6295af30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 16 May 2026 17:42:49 +0800 Subject: [PATCH] usbip: make ExportHost.Close stop event goroutines and Start transactional ExportHost.Events spawned goroutines tied to a caller-supplied ctx, and neither host.Close nor the per-error cleanup in ServerService.Start cancelled that ctx. A bind failure on port 3240 after Events succeeded leaked ueventLoop, the netlink fd, and the darwin watcher goroutine. The host now owns its background lifecycle: Start derives runCtx from the caller's ctx, Close cancels it, Events binds goroutines to it. ServerService.Start switches to a deferred cleanup that cancels and closes the host on any error, so future failure steps cannot forget it. --- service/usbip/host.go | 9 +++++---- service/usbip/host_darwin.go | 14 ++++++++++++-- service/usbip/host_linux.go | 20 +++++++++++++++++--- service/usbip/server.go | 15 +++++++++------ 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/service/usbip/host.go b/service/usbip/host.go index a86f9a19f..deaec1ab5 100644 --- a/service/usbip/host.go +++ b/service/usbip/host.go @@ -19,10 +19,11 @@ type ExportHost interface { Reconcile(ctx context.Context, isReserved func(busid string) bool) (snapshot map[string]Export, released []string, err error) FinishImport(ctx context.Context, busid string) (released bool, err error) // Events returns a coalescing channel that signals topology - // changes; the channel is closed when ctx is cancelled. A non-nil - // error means the host could not subscribe and the server must not - // continue. - Events(ctx context.Context) (<-chan struct{}, error) + // changes. The channel is closed when Close() is called, which + // also stops the host's background goroutines. Events MUST be + // called after Start succeeds. A non-nil error means the host + // could not subscribe and the server must not continue. + Events() (<-chan struct{}, error) } type ImportHost interface { diff --git a/service/usbip/host_darwin.go b/service/usbip/host_darwin.go index 57bc0fb08..a200cd933 100644 --- a/service/usbip/host_darwin.go +++ b/service/usbip/host_darwin.go @@ -34,6 +34,9 @@ type darwinExportHost struct { logger log.ContextLogger matches []option.USBIPDeviceMatch + runCtx context.Context + runCancel context.CancelFunc + access sync.Mutex exports map[string]*darwinExport watcher *darwinUSBHostDeviceWatcher @@ -48,10 +51,14 @@ func newDarwinExportHost(logger log.ContextLogger, matches []option.USBIPDeviceM } func (h *darwinExportHost) Start(ctx context.Context) error { + h.runCtx, h.runCancel = context.WithCancel(ctx) return nil } func (h *darwinExportHost) Close() error { + if h.runCancel != nil { + h.runCancel() + } h.access.Lock() watcher := h.watcher h.watcher = nil @@ -69,7 +76,10 @@ func (h *darwinExportHost) Close() error { return nil } -func (h *darwinExportHost) Events(ctx context.Context) (<-chan struct{}, error) { +func (h *darwinExportHost) Events() (<-chan struct{}, error) { + if h.runCtx == nil { + return nil, E.New("usbip host: Events called before Start") + } ch := make(chan struct{}, 1) signal := func() { select { @@ -86,7 +96,7 @@ func (h *darwinExportHost) Events(ctx context.Context) (<-chan struct{}, error) h.watcher = watcher h.access.Unlock() go func() { - <-ctx.Done() + <-h.runCtx.Done() h.access.Lock() w := h.watcher h.watcher = nil diff --git a/service/usbip/host_linux.go b/service/usbip/host_linux.go index a9d1510a3..5da8b0c7a 100644 --- a/service/usbip/host_linux.go +++ b/service/usbip/host_linux.go @@ -139,6 +139,9 @@ type linuxExportHost struct { logger log.ContextLogger matches []option.USBIPDeviceMatch + runCtx context.Context + runCancel context.CancelFunc + access sync.Mutex exports map[string]*linuxExport } @@ -159,10 +162,18 @@ func newLinuxExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMa } func (h *linuxExportHost) Start(ctx context.Context) error { - return ensureKernelPath(sysUsbipHostDriver, "usbip-host", "usbip-host driver") + err := ensureKernelPath(sysUsbipHostDriver, "usbip-host", "usbip-host driver") + if err != nil { + return err + } + h.runCtx, h.runCancel = context.WithCancel(ctx) + return nil } func (h *linuxExportHost) Close() error { + if h.runCancel != nil { + h.runCancel() + } h.access.Lock() exports := h.exports h.exports = make(map[string]*linuxExport) @@ -176,9 +187,12 @@ func (h *linuxExportHost) Close() error { return nil } -func (h *linuxExportHost) Events(ctx context.Context) (<-chan struct{}, error) { +func (h *linuxExportHost) Events() (<-chan struct{}, error) { + if h.runCtx == nil { + return nil, E.New("usbip host: Events called before Start") + } ch := make(chan struct{}, 1) - go h.ueventLoop(ctx, ch) + go h.ueventLoop(h.runCtx, ch) return ch, nil } diff --git a/service/usbip/server.go b/service/usbip/server.go index a993c3d8e..241943d7f 100644 --- a/service/usbip/server.go +++ b/service/usbip/server.go @@ -73,27 +73,30 @@ func NewServerService(ctx context.Context, logger log.ContextLogger, tag string, }, nil } -func (s *ServerService) Start(stage adapter.StartStage) error { +func (s *ServerService) Start(stage adapter.StartStage) (err error) { if stage != adapter.StartStateStart { return nil } - err := s.host.Start(s.ctx) + defer func() { + if err != nil { + s.cancel() + _ = s.host.Close() + } + }() + err = s.host.Start(s.ctx) if err != nil { return err } - events, err := s.host.Events(s.ctx) + events, err := s.host.Events() if err != nil { - _ = s.host.Close() return E.Cause(err, "subscribe topology events") } err = s.reconcileAndBroadcast(false) if err != nil { - _ = s.host.Close() return err } tcpListener, err := s.listener.ListenTCP() if err != nil { - _ = s.host.Close() return err } go s.acceptLoop(tcpListener)