b9cf48a827
Export.{Snapshot,LeaseCheck,DeviceInfo}, ExportHost.{Reconcile,FinishImport},
ImportHost.Start, and all 11 exportLedger methods carried ctx params that
implementations never consumed (linux FinishImport now reaches into h.runCtx
internally). UrbTransaction.{Wait,Cancel} did consume ctx, but every call site
passed context.Background(), forcing the reverse pattern in endpoint_darwin
where e.ctx was already cancelled. Cancel becomes synchronous, Wait reads
under the close(t.done) happens-before. ExportHost's runCtx now derives in
newPlatformExportHost so Start() is just precondition-check (linux
ensureKernelPath / darwin no-op), and the three pre-Start nil defences in
Close/Events fall out.
899 lines
24 KiB
Go
899 lines
24 KiB
Go
//go:build linux
|
|
|
|
package usbip
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func newPlatformExportHost(ctx context.Context, logger log.ContextLogger, matches []option.USBIPDeviceMatch) (ExportHost, error) {
|
|
return newLinuxExportHost(ctx, logger, matches), nil
|
|
}
|
|
|
|
func newPlatformImportHost(logger log.ContextLogger) (ImportHost, error) {
|
|
return &linuxImportHost{
|
|
logger: logger,
|
|
ports: make(map[int]struct{}),
|
|
}, nil
|
|
}
|
|
|
|
func isMissingUSBDeviceError(err error) bool {
|
|
return errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENODEV)
|
|
}
|
|
|
|
func linuxUSBIPStatusReason(status int) string {
|
|
switch status {
|
|
case usbipStatusAvailable:
|
|
return "available"
|
|
case usbipStatusUsed:
|
|
return "used"
|
|
case usbipStatusError:
|
|
return "error"
|
|
default:
|
|
return fmt.Sprintf("status=0x%08x", uint32(status))
|
|
}
|
|
}
|
|
|
|
type linuxExportIdentity struct {
|
|
BusNum uint32
|
|
DevNum uint32
|
|
Speed uint32
|
|
VendorID uint16
|
|
ProductID uint16
|
|
BCDDevice uint16
|
|
DeviceClass uint8
|
|
DeviceSubClass uint8
|
|
DeviceProtocol uint8
|
|
ConfigValue uint8
|
|
NumConfigs uint8
|
|
NumInterfaces uint8
|
|
Serial string
|
|
Interfaces []DeviceInterface
|
|
}
|
|
|
|
func newLinuxExportIdentity(descriptor sysfsDevice) linuxExportIdentity {
|
|
return linuxExportIdentity{
|
|
BusNum: descriptor.BusNum,
|
|
DevNum: descriptor.DevNum,
|
|
Speed: descriptor.Speed,
|
|
VendorID: descriptor.VendorID,
|
|
ProductID: descriptor.ProductID,
|
|
BCDDevice: descriptor.BCDDevice,
|
|
DeviceClass: descriptor.DeviceClass,
|
|
DeviceSubClass: descriptor.DeviceSubClass,
|
|
DeviceProtocol: descriptor.DeviceProtocol,
|
|
ConfigValue: descriptor.ConfigValue,
|
|
NumConfigs: descriptor.NumConfigs,
|
|
NumInterfaces: descriptor.NumInterfaces,
|
|
Serial: descriptor.Serial,
|
|
Interfaces: slices.Clone(descriptor.Interfaces),
|
|
}
|
|
}
|
|
|
|
func (i linuxExportIdentity) Equal(other linuxExportIdentity) bool {
|
|
if i.BusNum != other.BusNum ||
|
|
i.DevNum != other.DevNum ||
|
|
i.Speed != other.Speed ||
|
|
i.VendorID != other.VendorID ||
|
|
i.ProductID != other.ProductID ||
|
|
i.BCDDevice != other.BCDDevice ||
|
|
i.DeviceClass != other.DeviceClass ||
|
|
i.DeviceSubClass != other.DeviceSubClass ||
|
|
i.DeviceProtocol != other.DeviceProtocol ||
|
|
i.ConfigValue != other.ConfigValue ||
|
|
i.NumConfigs != other.NumConfigs ||
|
|
i.NumInterfaces != other.NumInterfaces ||
|
|
i.Serial != other.Serial ||
|
|
len(i.Interfaces) != len(other.Interfaces) {
|
|
return false
|
|
}
|
|
for index := range i.Interfaces {
|
|
if i.Interfaces[index] != other.Interfaces[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (i linuxExportIdentity) LeaseIdentity() ExportLeaseIdentity {
|
|
var builder strings.Builder
|
|
fmt.Fprintf(&builder, "linux:%d:%d:%d:%04x:%04x:%04x:%02x:%02x:%02x:%02x:%02x:%02x:%s",
|
|
i.BusNum,
|
|
i.DevNum,
|
|
i.Speed,
|
|
i.VendorID,
|
|
i.ProductID,
|
|
i.BCDDevice,
|
|
i.DeviceClass,
|
|
i.DeviceSubClass,
|
|
i.DeviceProtocol,
|
|
i.ConfigValue,
|
|
i.NumConfigs,
|
|
i.NumInterfaces,
|
|
i.Serial,
|
|
)
|
|
for _, iface := range i.Interfaces {
|
|
fmt.Fprintf(&builder, "|%02x.%02x.%02x", iface.BInterfaceClass, iface.BInterfaceSubClass, iface.BInterfaceProtocol)
|
|
}
|
|
return ExportLeaseIdentity(builder.String())
|
|
}
|
|
|
|
type linuxExportHost struct {
|
|
logger log.ContextLogger
|
|
matches []option.USBIPDeviceMatch
|
|
|
|
runCtx context.Context
|
|
runCancel context.CancelFunc
|
|
|
|
access sync.Mutex
|
|
exports map[string]*linuxExport
|
|
}
|
|
|
|
type linuxReconcilePlan struct {
|
|
toRelease []*linuxExport
|
|
toStale []string
|
|
toBind map[string]sysfsDevice
|
|
released []string
|
|
}
|
|
|
|
func newLinuxExportHost(ctx context.Context, logger log.ContextLogger, matches []option.USBIPDeviceMatch) *linuxExportHost {
|
|
runCtx, runCancel := context.WithCancel(ctx)
|
|
return &linuxExportHost{
|
|
runCtx: runCtx,
|
|
runCancel: runCancel,
|
|
logger: logger,
|
|
matches: matches,
|
|
exports: make(map[string]*linuxExport),
|
|
}
|
|
}
|
|
|
|
func (h *linuxExportHost) Start() error {
|
|
return ensureKernelPath(sysUsbipHostDriver, "usbip-host", "usbip-host driver")
|
|
}
|
|
|
|
func (h *linuxExportHost) Close() error {
|
|
h.runCancel()
|
|
h.access.Lock()
|
|
exports := h.exports
|
|
h.exports = make(map[string]*linuxExport)
|
|
h.access.Unlock()
|
|
for _, exp := range exports {
|
|
releaseErr := h.releaseExport(exp)
|
|
if releaseErr != nil {
|
|
h.logger.Warn("rollback ", exp.busid, ": ", releaseErr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *linuxExportHost) Events() (<-chan struct{}, error) {
|
|
ch := make(chan struct{}, 1)
|
|
go h.ueventLoop(h.runCtx, ch)
|
|
return ch, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) ueventLoop(ctx context.Context, ch chan<- struct{}) {
|
|
defer close(ch)
|
|
signal := func() {
|
|
select {
|
|
case ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
backoff := ueventListenerBackoffInitial
|
|
for {
|
|
listener, err := newUEventListener()
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
h.logger.Warn("open uevent listener: ", err)
|
|
if !sleepCtx(ctx, backoff) {
|
|
return
|
|
}
|
|
backoff *= 2
|
|
if backoff > ueventListenerBackoffMax {
|
|
backoff = ueventListenerBackoffMax
|
|
}
|
|
continue
|
|
}
|
|
backoff = ueventListenerBackoffInitial
|
|
listenerDone := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = listener.Close()
|
|
case <-listenerDone:
|
|
}
|
|
}()
|
|
signal()
|
|
for {
|
|
err = listener.WaitUSBEvent()
|
|
if err != nil {
|
|
close(listenerDone)
|
|
_ = listener.Close()
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
h.logger.Warn("read uevent: ", err)
|
|
if !sleepCtx(ctx, backoff) {
|
|
return
|
|
}
|
|
backoff *= 2
|
|
if backoff > ueventListenerBackoffMax {
|
|
backoff = ueventListenerBackoffMax
|
|
}
|
|
break
|
|
}
|
|
signal()
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
ueventListenerBackoffInitial = time.Second
|
|
ueventListenerBackoffMax = 30 * time.Second
|
|
)
|
|
|
|
func classifyLinuxReconcile(current map[string]*linuxExport, desired map[string]sysfsDevice, isReserved func(busid string) bool) linuxReconcilePlan {
|
|
remainingDesired := maps.Clone(desired)
|
|
plan := linuxReconcilePlan{
|
|
toBind: make(map[string]sysfsDevice),
|
|
}
|
|
for busid, exp := range current {
|
|
device, wanted := remainingDesired[busid]
|
|
reserved := isReserved(busid)
|
|
identityMatches := wanted && exp.identity.Equal(newLinuxExportIdentity(device))
|
|
switch {
|
|
case exp.stale:
|
|
if reserved {
|
|
delete(remainingDesired, busid)
|
|
continue
|
|
}
|
|
plan.toRelease = append(plan.toRelease, exp)
|
|
plan.released = append(plan.released, busid)
|
|
case identityMatches:
|
|
delete(remainingDesired, busid)
|
|
case reserved:
|
|
plan.toStale = append(plan.toStale, busid)
|
|
delete(remainingDesired, busid)
|
|
default:
|
|
plan.toRelease = append(plan.toRelease, exp)
|
|
plan.released = append(plan.released, busid)
|
|
}
|
|
}
|
|
for busid, device := range remainingDesired {
|
|
if isReserved(busid) {
|
|
continue
|
|
}
|
|
plan.toBind[busid] = device
|
|
}
|
|
return plan
|
|
}
|
|
|
|
func (h *linuxExportHost) Reconcile(isReserved func(busid string) bool) (map[string]Export, []string, error) {
|
|
devices, err := listUSBDevices()
|
|
if err != nil {
|
|
return h.snapshotSelf(), nil, E.Cause(err, "enumerate usb devices")
|
|
}
|
|
keys := make([]DeviceKey, len(devices))
|
|
for i := range devices {
|
|
keys[i] = DeviceKey{
|
|
BusID: devices[i].BusID,
|
|
VendorID: devices[i].VendorID,
|
|
ProductID: devices[i].ProductID,
|
|
Serial: devices[i].Serial,
|
|
}
|
|
}
|
|
desired := make(map[string]sysfsDevice)
|
|
for _, idx := range SelectMatches(h.matches, keys) {
|
|
path := devices[idx].Path
|
|
isVHCIImport := strings.Contains(path, "vhci_hcd")
|
|
if !isVHCIImport {
|
|
realPath, err := filepath.EvalSymlinks(path)
|
|
if err == nil {
|
|
isVHCIImport = strings.Contains(realPath, "vhci_hcd")
|
|
}
|
|
}
|
|
if isVHCIImport {
|
|
h.logger.Debug("skip vhci-imported device ", devices[idx].BusID)
|
|
continue
|
|
}
|
|
if devices[idx].DeviceClass == 0x09 {
|
|
h.logger.Warn("skip hub device ", devices[idx].BusID)
|
|
continue
|
|
}
|
|
desired[devices[idx].BusID] = devices[idx]
|
|
}
|
|
|
|
h.access.Lock()
|
|
current := make(map[string]*linuxExport, len(h.exports))
|
|
maps.Copy(current, h.exports)
|
|
h.access.Unlock()
|
|
|
|
plan := classifyLinuxReconcile(current, desired, isReserved)
|
|
committed := make(map[string]*linuxExport, len(current)+len(plan.toBind))
|
|
maps.Copy(committed, current)
|
|
var reconcileErrors []error
|
|
|
|
for _, busid := range plan.toStale {
|
|
exp, found := committed[busid]
|
|
if !found {
|
|
continue
|
|
}
|
|
cloned := cloneLinuxExport(exp)
|
|
cloned.stale = true
|
|
committed[busid] = cloned
|
|
}
|
|
|
|
for _, exp := range plan.toRelease {
|
|
releaseErr := h.releaseExport(exp)
|
|
if releaseErr != nil {
|
|
h.logger.Warn("release ", exp.busid, ": ", releaseErr)
|
|
reconcileErrors = append(reconcileErrors, E.Cause(releaseErr, "release ", exp.busid))
|
|
}
|
|
var desiredDevice *sysfsDevice
|
|
desiredEntry, found := desired[exp.busid]
|
|
if found {
|
|
desiredDevice = &desiredEntry
|
|
}
|
|
resolved, resolveErr := h.resolveCommittedRelease(exp, desiredDevice)
|
|
if resolveErr != nil {
|
|
reconcileErrors = append(reconcileErrors, resolveErr)
|
|
}
|
|
if resolved == nil {
|
|
delete(committed, exp.busid)
|
|
continue
|
|
}
|
|
committed[exp.busid] = resolved
|
|
}
|
|
|
|
for busid, device := range plan.toBind {
|
|
_, found := committed[busid]
|
|
if found {
|
|
continue
|
|
}
|
|
previousDriver, probeErr := currentDriver(busid)
|
|
if probeErr != nil {
|
|
reconcileErrors = append(reconcileErrors, E.Cause(probeErr, "probe driver before bind ", busid))
|
|
}
|
|
exp, bindErr := h.bindOne(&device)
|
|
if bindErr == nil {
|
|
committed[busid] = exp
|
|
continue
|
|
}
|
|
reconcileErrors = append(reconcileErrors, E.Cause(bindErr, "bind ", busid))
|
|
resolved, resolveErr := h.resolveCommittedBind(busid, &device, previousDriver)
|
|
if resolveErr != nil {
|
|
reconcileErrors = append(reconcileErrors, resolveErr)
|
|
}
|
|
if resolved != nil {
|
|
committed[busid] = resolved
|
|
}
|
|
}
|
|
|
|
released := make([]string, 0, len(plan.released))
|
|
for _, busid := range plan.released {
|
|
exp, found := committed[busid]
|
|
if found && !exp.stale {
|
|
continue
|
|
}
|
|
released = append(released, busid)
|
|
}
|
|
|
|
h.access.Lock()
|
|
h.exports = committed
|
|
h.access.Unlock()
|
|
|
|
return snapshotLinuxExports(committed), released, E.Errors(reconcileErrors...)
|
|
}
|
|
|
|
func (h *linuxExportHost) FinishImport(busid string) (bool, error) {
|
|
err := writeSysfs(filepath.Join(sysBusUSBDevices, busid, "usbip_sockfd"), "-1")
|
|
if err != nil && !os.IsNotExist(err) && !isMissingUSBDeviceError(err) {
|
|
h.logger.Debug("release ", busid, " from usbip-host: ", err)
|
|
}
|
|
waitForUsbipStatusCleared(h.runCtx, busid)
|
|
h.access.Lock()
|
|
exp, ok := h.exports[busid]
|
|
h.access.Unlock()
|
|
if !ok || !exp.stale {
|
|
return false, nil
|
|
}
|
|
releaseErr := h.releaseExport(exp)
|
|
h.access.Lock()
|
|
current, stillPresent := h.exports[busid]
|
|
if stillPresent && current == exp {
|
|
delete(h.exports, busid)
|
|
}
|
|
h.access.Unlock()
|
|
if releaseErr != nil {
|
|
h.logger.Warn("release stale ", busid, ": ", releaseErr)
|
|
}
|
|
return true, E.Errors(err, releaseErr)
|
|
}
|
|
|
|
func (h *linuxExportHost) snapshotSelf() map[string]Export {
|
|
h.access.Lock()
|
|
defer h.access.Unlock()
|
|
return snapshotLinuxExports(h.exports)
|
|
}
|
|
|
|
// snapshotLinuxExports returns every tracked export, including stale
|
|
// ones. The ledger treats stale entries as broadcastable State:
|
|
// unavailable updates via Export.Snapshot, which is what the
|
|
// ExportSnapshot contract requires; filtering here would surface a
|
|
// removed device instead of an updated one.
|
|
func snapshotLinuxExports(exports map[string]*linuxExport) map[string]Export {
|
|
out := make(map[string]Export, len(exports))
|
|
for busid, exp := range exports {
|
|
out[busid] = exp
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneLinuxExport(exp *linuxExport) *linuxExport {
|
|
if exp == nil {
|
|
return nil
|
|
}
|
|
clone := *exp
|
|
clone.descriptor.Interfaces = slices.Clone(exp.descriptor.Interfaces)
|
|
clone.identity.Interfaces = slices.Clone(exp.identity.Interfaces)
|
|
return &clone
|
|
}
|
|
|
|
func (h *linuxExportHost) resolveCommittedRelease(exp *linuxExport, desired *sysfsDevice) (*linuxExport, error) {
|
|
if desired != nil {
|
|
resolved, found, err := h.probeDesiredBoundExport(exp.busid, desired, exp.managed, exp.originalDriver)
|
|
if err != nil {
|
|
return exp, err
|
|
}
|
|
if found {
|
|
return resolved, nil
|
|
}
|
|
}
|
|
driver, err := currentDriver(exp.busid)
|
|
if err != nil {
|
|
return exp, E.Cause(err, "probe driver ", exp.busid)
|
|
}
|
|
if driver != "usbip-host" {
|
|
return nil, nil
|
|
}
|
|
return exp, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) resolveCommittedBind(busid string, desired *sysfsDevice, originalDriver string) (*linuxExport, error) {
|
|
if desired == nil {
|
|
return nil, nil
|
|
}
|
|
resolved, found, err := h.probeDesiredBoundExport(busid, desired, true, originalDriver)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, nil
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) probeDesiredBoundExport(busid string, desired *sysfsDevice, managed bool, originalDriver string) (*linuxExport, bool, error) {
|
|
driver, err := currentDriver(busid)
|
|
if err != nil {
|
|
return nil, false, E.Cause(err, "probe driver ", busid)
|
|
}
|
|
if driver != "usbip-host" {
|
|
return nil, false, nil
|
|
}
|
|
descriptor, err := readSysfsDevice(busid, filepath.Join(sysBusUSBDevices, busid))
|
|
if err != nil {
|
|
descriptor = *desired
|
|
} else if !newLinuxExportIdentity(descriptor).Equal(newLinuxExportIdentity(*desired)) {
|
|
return nil, false, nil
|
|
}
|
|
return h.newExport(descriptor, managed, originalDriver), true, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) bindOne(d *sysfsDevice) (*linuxExport, error) {
|
|
var (
|
|
exp *linuxExport
|
|
err error
|
|
)
|
|
for attempt := range 2 {
|
|
exp, err = h.bindOneOnce(d)
|
|
if err == nil {
|
|
return exp, nil
|
|
}
|
|
if attempt > 0 || !errors.Is(err, unix.ENODEV) {
|
|
break
|
|
}
|
|
h.logger.Warn("reset usbip-host after bind failure on ", d.BusID, ": ", err)
|
|
h.access.Lock()
|
|
active := len(h.exports) > 0
|
|
h.access.Unlock()
|
|
if active {
|
|
return nil, E.Cause(E.New("active usbip-host exports are present"), "reset usbip-host after bind failure")
|
|
}
|
|
resetErr := reloadHostDriver()
|
|
if resetErr != nil {
|
|
return nil, E.Cause(resetErr, "reset usbip-host after bind failure")
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func (h *linuxExportHost) bindOneOnce(d *sysfsDevice) (*linuxExport, error) {
|
|
driver, err := currentDriver(d.BusID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if driver == "usbip-host" {
|
|
h.logger.Info("device ", d.BusID, " already bound to usbip-host; co-opting")
|
|
return h.newExport(*d, false, ""), nil
|
|
}
|
|
if driver != "" {
|
|
err = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "unbind"), d.BusID)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "unbind from ", driver)
|
|
}
|
|
}
|
|
matchBusIDPath := filepath.Join(sysUsbipHostDriver, "match_busid")
|
|
err = writeSysfs(matchBusIDPath, "add "+d.BusID)
|
|
if err != nil {
|
|
if driver != "" {
|
|
_ = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "bind"), d.BusID)
|
|
}
|
|
return nil, E.Cause(err, "match_busid add")
|
|
}
|
|
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "bind"), d.BusID)
|
|
if err != nil {
|
|
_ = writeSysfs(matchBusIDPath, "del "+d.BusID)
|
|
if driver != "" {
|
|
_ = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "bind"), d.BusID)
|
|
}
|
|
return nil, E.Cause(err, "bind to usbip-host")
|
|
}
|
|
previousDriver := driver
|
|
if previousDriver == "" {
|
|
previousDriver = "(no driver)"
|
|
}
|
|
h.logger.Info("exported ", d.BusID, " (previously on ", previousDriver, ")")
|
|
return h.newExport(*d, true, driver), nil
|
|
}
|
|
|
|
func (h *linuxExportHost) releaseExport(exp *linuxExport) error {
|
|
if !exp.managed {
|
|
h.logger.Info("stopped tracking ", exp.busid, " on usbip-host")
|
|
return nil
|
|
}
|
|
status, statusErr := readUsbipStatus(exp.busid)
|
|
if statusErr != nil && !os.IsNotExist(statusErr) && !isMissingUSBDeviceError(statusErr) {
|
|
return statusErr
|
|
}
|
|
if statusErr == nil && status == usbipStatusUsed {
|
|
err := writeSysfs(filepath.Join(sysBusUSBDevices, exp.busid, "usbip_sockfd"), "-1")
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
err := writeSysfs(filepath.Join(sysUsbipHostDriver, "unbind"), exp.busid)
|
|
if err != nil && !os.IsNotExist(err) && !isMissingUSBDeviceError(err) {
|
|
return err
|
|
}
|
|
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "match_busid"), "del "+exp.busid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
restoreCurrentDevice, err := h.shouldRestoreCurrentDevice(exp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !restoreCurrentDevice {
|
|
h.logger.Info("removed export state for ", exp.busid)
|
|
return nil
|
|
}
|
|
if exp.originalDriver == "" {
|
|
h.logger.Info("released ", exp.busid, " from usbip-host")
|
|
return nil
|
|
}
|
|
err = writeSysfs(filepath.Join("/sys/bus/usb/drivers", exp.originalDriver, "bind"), exp.busid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.logger.Info("restored ", exp.busid, " to ", exp.originalDriver)
|
|
return nil
|
|
}
|
|
|
|
func (h *linuxExportHost) shouldRestoreCurrentDevice(exp *linuxExport) (bool, error) {
|
|
descriptor, err := readSysfsDevice(exp.busid, filepath.Join(sysBusUSBDevices, exp.busid))
|
|
if err != nil {
|
|
if os.IsNotExist(err) || isMissingUSBDeviceError(err) {
|
|
return false, nil
|
|
}
|
|
return false, E.Cause(err, "read current device ", exp.busid)
|
|
}
|
|
return exp.identity.Equal(newLinuxExportIdentity(descriptor)), nil
|
|
}
|
|
|
|
func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, originalDriver string) *linuxExport {
|
|
return &linuxExport{
|
|
busid: descriptor.BusID,
|
|
descriptor: descriptor,
|
|
identity: newLinuxExportIdentity(descriptor),
|
|
managed: managed,
|
|
originalDriver: originalDriver,
|
|
logger: h.logger,
|
|
}
|
|
}
|
|
|
|
// linuxExport caches the bind-time descriptor because it is immutable
|
|
// post-enumeration; Snapshot only re-reads usbip_status.
|
|
type linuxExport struct {
|
|
busid string
|
|
descriptor sysfsDevice
|
|
identity linuxExportIdentity
|
|
managed bool
|
|
originalDriver string
|
|
logger log.ContextLogger
|
|
stale bool
|
|
}
|
|
|
|
func (e *linuxExport) BusID() string {
|
|
return e.busid
|
|
}
|
|
|
|
func (e *linuxExport) LeaseIdentity() ExportLeaseIdentity {
|
|
return e.identity.LeaseIdentity()
|
|
}
|
|
|
|
func (e *linuxExport) Snapshot(busy bool) ExportSnapshot {
|
|
stableID := "linux-busid:" + e.descriptor.BusID
|
|
if e.descriptor.Serial != "" {
|
|
stableID = fmt.Sprintf("usb:%04x:%04x:%s", e.descriptor.VendorID, e.descriptor.ProductID, e.descriptor.Serial)
|
|
}
|
|
if e.stale {
|
|
return ExportSnapshot{
|
|
Entry: DeviceEntry{
|
|
Info: e.descriptor.toProtocol(),
|
|
Interfaces: e.descriptor.Interfaces,
|
|
Serial: e.descriptor.Serial,
|
|
},
|
|
Backend: backendIDLinuxSysfs,
|
|
StableID: stableID,
|
|
State: deviceStateUnavailable,
|
|
StatusReason: "device replaced",
|
|
}
|
|
}
|
|
status, statusErr := readUsbipStatus(e.busid)
|
|
var state, reason string
|
|
switch {
|
|
case statusErr != nil:
|
|
state = deviceStateUnavailable
|
|
reason = statusErr.Error()
|
|
case busy:
|
|
status = usbipStatusUsed
|
|
state = deviceStateBusy
|
|
reason = linuxUSBIPStatusReason(status)
|
|
case status == usbipStatusAvailable:
|
|
state = deviceStateAvailable
|
|
reason = linuxUSBIPStatusReason(status)
|
|
case status == usbipStatusUsed:
|
|
state = deviceStateBusy
|
|
reason = linuxUSBIPStatusReason(status)
|
|
default:
|
|
state = deviceStateUnavailable
|
|
reason = linuxUSBIPStatusReason(status)
|
|
}
|
|
return ExportSnapshot{
|
|
Entry: DeviceEntry{
|
|
Info: e.descriptor.toProtocol(),
|
|
Interfaces: e.descriptor.Interfaces,
|
|
Serial: e.descriptor.Serial,
|
|
},
|
|
Backend: backendIDLinuxSysfs,
|
|
StableID: stableID,
|
|
State: state,
|
|
StatusReason: reason,
|
|
RawStatus: status,
|
|
}
|
|
}
|
|
|
|
func (e *linuxExport) LeaseCheck() (bool, string) {
|
|
if e.stale {
|
|
return false, "device replaced"
|
|
}
|
|
status, err := readUsbipStatus(e.busid)
|
|
if err != nil {
|
|
return false, err.Error()
|
|
}
|
|
if status != usbipStatusAvailable {
|
|
return false, linuxUSBIPStatusReason(status)
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
func (e *linuxExport) DeviceInfo() (DeviceInfoTruncated, error) {
|
|
return e.descriptor.toProtocol(), nil
|
|
}
|
|
|
|
func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
|
|
if e.stale {
|
|
return nil, E.New("linux export ", e.busid, " is stale")
|
|
}
|
|
handoff, err := newKernelHandoffSession(ctx, conn, e.logger, "server", e.busid)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "prepare handoff")
|
|
}
|
|
mode := "direct"
|
|
if handoff.relayConn != nil {
|
|
mode = "relay"
|
|
}
|
|
e.logger.Debug("usbip server handoff ", e.busid, ": ", mode)
|
|
err = writeSysfs(filepath.Join(sysBusUSBDevices, e.busid, "usbip_sockfd"), strconv.Itoa(int(handoff.file.Fd())))
|
|
if err != nil {
|
|
_ = handoff.Close()
|
|
return nil, E.Cause(err, "hand off ", e.busid, " to kernel")
|
|
}
|
|
closeErr := handoff.closeKernelFD()
|
|
if closeErr != nil {
|
|
e.logger.Debug("close kernel fd ", e.busid, ": ", closeErr)
|
|
}
|
|
return handoff, nil
|
|
}
|
|
|
|
type linuxImportHost struct {
|
|
logger log.ContextLogger
|
|
|
|
portsAccess sync.Mutex
|
|
ports map[int]struct{}
|
|
}
|
|
|
|
func (h *linuxImportHost) Start() error {
|
|
return ensureKernelPath(sysVHCIControllerV0, "vhci-hcd", "vhci_hcd.0")
|
|
}
|
|
|
|
func (h *linuxImportHost) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (h *linuxImportHost) Attach(ctx context.Context, info DeviceInfoTruncated, conn net.Conn) (AttachedSession, error) {
|
|
handoff, err := newKernelHandoffSession(ctx, conn, h.logger, "client", info.BusIDString())
|
|
if err != nil {
|
|
return nil, E.Cause(err, "prepare handoff")
|
|
}
|
|
mode := "direct"
|
|
if handoff.relayConn != nil {
|
|
mode = "relay"
|
|
}
|
|
h.logger.Debug("usbip client handoff ", info.BusIDString(), ": ", mode)
|
|
port, secondary, attachErr := h.attachOnce(ctx, info, handoff)
|
|
if attachErr != nil {
|
|
_ = handoff.Close()
|
|
return nil, attachErr
|
|
}
|
|
_ = handoff.Start()
|
|
return &linuxClientSession{
|
|
handoff: handoff,
|
|
host: h,
|
|
port: port,
|
|
secondary: secondary,
|
|
}, nil
|
|
}
|
|
|
|
func (h *linuxImportHost) attachOnce(ctx context.Context, info DeviceInfoTruncated, handoff *kernelHandoffSession) (int, int, error) {
|
|
triedPorts := make(map[int]struct{})
|
|
for {
|
|
port, err := vhciPickFreePort(info.Speed, triedPorts)
|
|
if err != nil {
|
|
return -1, 0, err
|
|
}
|
|
if !h.reservePort(port) {
|
|
triedPorts[port] = 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)
|
|
if err != nil {
|
|
h.releasePort(port)
|
|
if errors.Is(err, unix.EBUSY) {
|
|
triedPorts[port] = struct{}{}
|
|
continue
|
|
}
|
|
return -1, 0, E.Cause(err, "vhci attach")
|
|
}
|
|
err = handoff.closeKernelFD()
|
|
if err != nil {
|
|
h.logger.Debug("close kernel fd ", info.BusIDString(), ": ", err)
|
|
}
|
|
return port, lookupSecondaryForPort(port), nil
|
|
}
|
|
}
|
|
|
|
func lookupSecondaryForPort(port int) int {
|
|
records, err := readPrimaryVHCIStatus()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
for _, record := range records {
|
|
if record.port == port {
|
|
return record.secondary
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (h *linuxImportHost) reservePort(port int) bool {
|
|
h.portsAccess.Lock()
|
|
defer h.portsAccess.Unlock()
|
|
_, exists := h.ports[port]
|
|
if exists {
|
|
h.logger.Debug("vhci port ", port, " already reserved locally")
|
|
return false
|
|
}
|
|
h.logger.Debug("reserve vhci port ", port)
|
|
h.ports[port] = struct{}{}
|
|
return true
|
|
}
|
|
|
|
func (h *linuxImportHost) releasePort(port int) {
|
|
h.portsAccess.Lock()
|
|
defer h.portsAccess.Unlock()
|
|
h.logger.Debug("release vhci port ", port)
|
|
delete(h.ports, port)
|
|
}
|
|
|
|
type linuxClientSession struct {
|
|
handoff *kernelHandoffSession
|
|
host *linuxImportHost
|
|
port int
|
|
secondary int
|
|
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
func (s *linuxClientSession) Done() <-chan struct{} {
|
|
return s.handoff.Done()
|
|
}
|
|
|
|
func (s *linuxClientSession) Err() error {
|
|
return s.handoff.Err()
|
|
}
|
|
|
|
func (s *linuxClientSession) Start() error {
|
|
return s.handoff.Start()
|
|
}
|
|
|
|
func (s *linuxClientSession) Close() error {
|
|
s.closeOnce.Do(func() {
|
|
detachErr := writeSysfs(filepath.Join(sysVHCIControllerV0, "detach"), strconv.Itoa(s.port))
|
|
closeErr := s.handoff.Close()
|
|
s.host.releasePort(s.port)
|
|
s.closeErr = E.Errors(detachErr, closeErr)
|
|
})
|
|
return s.closeErr
|
|
}
|
|
|
|
func (s *linuxClientSession) Description() string {
|
|
if s.secondary == 0 {
|
|
return fmt.Sprintf("vhci_hcd.0 port %d", s.port)
|
|
}
|
|
return fmt.Sprintf("vhci_hcd.0 (controller %d) port %d", s.secondary, s.port)
|
|
}
|