usbip: drain linux stub status on finish-import; iterate vhci controllers
This commit is contained in:
+57
-33
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user