usbip: drain linux stub status on finish-import; iterate vhci controllers

This commit is contained in:
世界
2026-05-15 14:06:31 +08:00
parent d741a6a7e9
commit cb9ec99e04
3 changed files with 169 additions and 78 deletions
+57 -33
View File
@@ -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)
}
+6 -25
View File
@@ -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
}
+106 -20
View File
@@ -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