mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-30 13:21:49 +00:00
usbip: fix lease identity, linux replacement, and darwin stalls
This commit is contained in:
parent
8799b10b9a
commit
249ba662c4
11 changed files with 534 additions and 88 deletions
|
|
@ -127,6 +127,7 @@ type serverImportLease struct {
|
|||
BusID string
|
||||
ClientNonce uint64
|
||||
Generation uint64
|
||||
Identity ExportLeaseIdentity
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ func (l *exportLedger) TryReserveForImport(ctx context.Context, busid string) (E
|
|||
if !found {
|
||||
return nil, false, "unknown busid"
|
||||
}
|
||||
identity := export.LeaseIdentity()
|
||||
if busy || leased {
|
||||
return nil, false, deviceStateBusy
|
||||
}
|
||||
|
|
@ -167,7 +168,7 @@ func (l *exportLedger) TryReserveForImport(ctx context.Context, busid string) (E
|
|||
l.slow.Lock()
|
||||
defer l.slow.Unlock()
|
||||
current, stillExported := l.exports[busid]
|
||||
if !stillExported || current != export {
|
||||
if !stillExported || current.LeaseIdentity() != identity {
|
||||
return nil, false, "unknown busid"
|
||||
}
|
||||
if l.busy[busid] {
|
||||
|
|
@ -190,9 +191,9 @@ func (l *exportLedger) ReleaseImport(ctx context.Context, busid string, removeEx
|
|||
l.BroadcastIfChanged(ctx)
|
||||
}
|
||||
|
||||
// IssueLease captures the seq generation at entry so a subsequent
|
||||
// ConsumeLeaseAndReserve can reject stale leases issued before a
|
||||
// topology change.
|
||||
// IssueLease captures the current broadcast sequence as opaque metadata
|
||||
// for control clients. Lease correctness itself is pinned to the
|
||||
// export's internal identity, not to that sequence number.
|
||||
func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request controlLeaseRequest) controlLeaseResponse {
|
||||
response := controlLeaseResponse{
|
||||
BusID: request.BusID,
|
||||
|
|
@ -232,6 +233,7 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
|
|||
}
|
||||
l.slow.Unlock()
|
||||
|
||||
identity := export.LeaseIdentity()
|
||||
leaseOK, leaseReason := export.LeaseCheck(ctx)
|
||||
if !leaseOK {
|
||||
response.ErrorCode = leaseErrorUnavailable
|
||||
|
|
@ -244,7 +246,7 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
|
|||
now = l.now()
|
||||
l.cleanupExpiredLocked(now)
|
||||
current, stillExported := l.exports[request.BusID]
|
||||
if !stillExported || current != export {
|
||||
if !stillExported || current.LeaseIdentity() != identity {
|
||||
response.ErrorCode = leaseErrorUnavailable
|
||||
response.ErrorMessage = "unknown busid"
|
||||
return response
|
||||
|
|
@ -266,6 +268,7 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
|
|||
BusID: request.BusID,
|
||||
ClientNonce: request.ClientNonce,
|
||||
Generation: generation,
|
||||
Identity: current.LeaseIdentity(),
|
||||
Expires: now.Add(l.ttl),
|
||||
}
|
||||
l.leases[request.BusID] = lease
|
||||
|
|
@ -275,60 +278,101 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
|
|||
return response
|
||||
}
|
||||
|
||||
// ConsumeLeaseAndReserve atomically validates a lease, removes its entry,
|
||||
// and marks the busid busy under a single slow-lock critical section so
|
||||
// no concurrent import can observe the consume-before-reserve gap, and
|
||||
// no concurrent broadcast can observe a transient busy=true that we
|
||||
// later roll back. The generation snapshot is taken before the slow
|
||||
// critical section so the comparison either matches the lease exactly
|
||||
// or is strictly stale (the safe rejection direction): a broadcast that
|
||||
// fires between the snapshot and slow.Lock can only advance seq.
|
||||
// ConsumeLeaseAndReserve validates the requested lease against the
|
||||
// current export identity, reruns LeaseCheck outside the slow lock, then
|
||||
// atomically consumes the lease and marks the busid busy. Correctness is
|
||||
// tied to the export identity rather than to the control sequence.
|
||||
//
|
||||
// Consume-on-read semantics from the old ConsumeLease are preserved:
|
||||
// the lease entry is removed on every outcome except a nonce/ID
|
||||
// mismatch (which preserves the lease for the legitimate holder).
|
||||
// LeaseCheck is not re-run — the lease attests that it passed at issue
|
||||
// time, and the generation equality test confirms the export has not
|
||||
// been reconciled since. The caller must pair every success with a
|
||||
// later ReleaseImport.
|
||||
func (l *exportLedger) ConsumeLeaseAndReserve(request ImportExtRequest) (Export, bool, string) {
|
||||
l.fast.Lock()
|
||||
currentGeneration := l.seq
|
||||
l.fast.Unlock()
|
||||
|
||||
// The caller must pair every success with a later ReleaseImport.
|
||||
func (l *exportLedger) ConsumeLeaseAndReserve(ctx context.Context, request ImportExtRequest) (Export, bool, string) {
|
||||
var (
|
||||
export Export
|
||||
identity ExportLeaseIdentity
|
||||
)
|
||||
l.slow.Lock()
|
||||
defer l.slow.Unlock()
|
||||
|
||||
now := l.now()
|
||||
l.cleanupExpiredLocked(now)
|
||||
|
||||
lease, found := l.leases[request.BusID]
|
||||
if !found {
|
||||
l.slow.Unlock()
|
||||
return nil, false, "lease not found"
|
||||
}
|
||||
if lease.ID != request.LeaseID || lease.ClientNonce != request.ClientNonce {
|
||||
l.slow.Unlock()
|
||||
return nil, false, "lease mismatch"
|
||||
}
|
||||
if !now.Before(lease.Expires) {
|
||||
delete(l.leases, request.BusID)
|
||||
l.slow.Unlock()
|
||||
return nil, false, "lease expired"
|
||||
}
|
||||
export, stillExported := l.exports[request.BusID]
|
||||
if !stillExported {
|
||||
delete(l.leases, request.BusID)
|
||||
l.slow.Unlock()
|
||||
return nil, false, "unknown busid"
|
||||
}
|
||||
identity = export.LeaseIdentity()
|
||||
if identity != lease.Identity {
|
||||
delete(l.leases, request.BusID)
|
||||
l.slow.Unlock()
|
||||
return nil, false, "lease stale"
|
||||
}
|
||||
if l.busy[request.BusID] {
|
||||
delete(l.leases, request.BusID)
|
||||
l.slow.Unlock()
|
||||
return nil, false, deviceStateBusy
|
||||
}
|
||||
l.slow.Unlock()
|
||||
|
||||
leaseOK, leaseReason := export.LeaseCheck(ctx)
|
||||
if !leaseOK {
|
||||
l.slow.Lock()
|
||||
currentLease, exists := l.leases[request.BusID]
|
||||
if exists && currentLease.ID == request.LeaseID && currentLease.ClientNonce == request.ClientNonce {
|
||||
delete(l.leases, request.BusID)
|
||||
}
|
||||
l.slow.Unlock()
|
||||
return nil, false, leaseReason
|
||||
}
|
||||
|
||||
l.slow.Lock()
|
||||
defer l.slow.Unlock()
|
||||
|
||||
now = l.now()
|
||||
l.cleanupExpiredLocked(now)
|
||||
|
||||
lease, found = l.leases[request.BusID]
|
||||
if !found {
|
||||
return nil, false, "lease not found"
|
||||
}
|
||||
if lease.ID != request.LeaseID || lease.ClientNonce != request.ClientNonce {
|
||||
return nil, false, "lease mismatch"
|
||||
}
|
||||
if lease.Generation != currentGeneration {
|
||||
delete(l.leases, request.BusID)
|
||||
return nil, false, "lease stale"
|
||||
}
|
||||
if !now.Before(lease.Expires) {
|
||||
delete(l.leases, request.BusID)
|
||||
return nil, false, "lease expired"
|
||||
}
|
||||
export, stillExported := l.exports[request.BusID]
|
||||
current, stillExported := l.exports[request.BusID]
|
||||
if !stillExported {
|
||||
delete(l.leases, request.BusID)
|
||||
return nil, false, "unknown busid"
|
||||
}
|
||||
if lease.Identity != identity || current.LeaseIdentity() != identity {
|
||||
delete(l.leases, request.BusID)
|
||||
return nil, false, "lease stale"
|
||||
}
|
||||
if l.busy[request.BusID] {
|
||||
delete(l.leases, request.BusID)
|
||||
return nil, false, deviceStateBusy
|
||||
}
|
||||
delete(l.leases, request.BusID)
|
||||
l.busy[request.BusID] = true
|
||||
return export, true, ""
|
||||
return current, true, ""
|
||||
}
|
||||
|
||||
func (l *exportLedger) cleanupExpiredLocked(now time.Time) {
|
||||
|
|
|
|||
|
|
@ -53,11 +53,105 @@ func TestSubscribeRetriesSnapshotWhenSequenceAdvances(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConsumeLeaseAndReserveRejectsIdentityReplacement(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ledger := newExportLedger(nil, time.Second, func() time.Time { return time.Unix(0, 0) })
|
||||
original := &testExport{busid: "1-1", vendorID: 0x1111, productID: 0x0001, identity: "linux:original"}
|
||||
replacement := &testExport{busid: "1-1", vendorID: 0x1111, productID: 0x0001, identity: "linux:replacement"}
|
||||
|
||||
ledger.ApplyHostSnapshot(map[string]Export{original.busid: original}, nil)
|
||||
lease := ledger.IssueLease(ctx, 1, controlLeaseRequest{BusID: original.busid, ClientNonce: 7})
|
||||
ledger.ApplyHostSnapshot(map[string]Export{replacement.busid: replacement}, nil)
|
||||
|
||||
_, ok, reason := ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
|
||||
BusID: original.busid,
|
||||
LeaseID: lease.LeaseID,
|
||||
ClientNonce: lease.ClientNonce,
|
||||
})
|
||||
if ok {
|
||||
t.Fatal("expected replaced export lease to be rejected")
|
||||
}
|
||||
if reason != "lease stale" {
|
||||
t.Fatalf("expected lease stale, got %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeLeaseAndReserveRejectsUnavailableExport(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ledger := newExportLedger(nil, time.Second, func() time.Time { return time.Unix(0, 0) })
|
||||
available := true
|
||||
exp := &testExport{
|
||||
busid: "1-1",
|
||||
vendorID: 0x1111,
|
||||
productID: 0x0001,
|
||||
leaseCheck: func(context.Context) (bool, string) {
|
||||
if available {
|
||||
return true, ""
|
||||
}
|
||||
return false, "capture released"
|
||||
},
|
||||
}
|
||||
|
||||
ledger.ApplyHostSnapshot(map[string]Export{exp.busid: exp}, nil)
|
||||
lease := ledger.IssueLease(ctx, 1, controlLeaseRequest{BusID: exp.busid, ClientNonce: 9})
|
||||
available = false
|
||||
|
||||
_, ok, reason := ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
|
||||
BusID: exp.busid,
|
||||
LeaseID: lease.LeaseID,
|
||||
ClientNonce: lease.ClientNonce,
|
||||
})
|
||||
if ok {
|
||||
t.Fatal("expected unavailable export lease to be rejected")
|
||||
}
|
||||
if reason != "capture released" {
|
||||
t.Fatalf("expected capture released, got %q", reason)
|
||||
}
|
||||
|
||||
_, ok, reason = ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
|
||||
BusID: exp.busid,
|
||||
LeaseID: lease.LeaseID,
|
||||
ClientNonce: lease.ClientNonce,
|
||||
})
|
||||
if ok {
|
||||
t.Fatal("expected consumed lease to stay unavailable on retry")
|
||||
}
|
||||
if reason != "lease not found" {
|
||||
t.Fatalf("expected consumed lease to disappear, got %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeLeaseAndReserveMarksBusyOnSuccess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ledger := newExportLedger(nil, time.Second, func() time.Time { return time.Unix(0, 0) })
|
||||
exp := &testExport{busid: "1-1", vendorID: 0x1111, productID: 0x0001}
|
||||
|
||||
ledger.ApplyHostSnapshot(map[string]Export{exp.busid: exp}, nil)
|
||||
lease := ledger.IssueLease(ctx, 1, controlLeaseRequest{BusID: exp.busid, ClientNonce: 11})
|
||||
|
||||
reserved, ok, reason := ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
|
||||
BusID: exp.busid,
|
||||
LeaseID: lease.LeaseID,
|
||||
ClientNonce: lease.ClientNonce,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatalf("expected lease reservation success, got %q", reason)
|
||||
}
|
||||
if reserved != exp {
|
||||
t.Fatal("expected to reserve the original export instance")
|
||||
}
|
||||
if !ledger.IsBusy(exp.busid) {
|
||||
t.Fatal("expected successful lease reservation to mark busid busy")
|
||||
}
|
||||
}
|
||||
|
||||
type testExport struct {
|
||||
busid string
|
||||
vendorID uint16
|
||||
productID uint16
|
||||
|
||||
identity ExportLeaseIdentity
|
||||
leaseCheck func(context.Context) (bool, string)
|
||||
onSnapshot func()
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +159,13 @@ func (e *testExport) BusID() string {
|
|||
return e.busid
|
||||
}
|
||||
|
||||
func (e *testExport) LeaseIdentity() ExportLeaseIdentity {
|
||||
if e.identity != "" {
|
||||
return e.identity
|
||||
}
|
||||
return ExportLeaseIdentity(e.busid)
|
||||
}
|
||||
|
||||
func (e *testExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
|
||||
onSnapshot := e.onSnapshot
|
||||
e.onSnapshot = nil
|
||||
|
|
@ -80,6 +181,9 @@ func (e *testExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
|
|||
}
|
||||
|
||||
func (e *testExport) LeaseCheck(ctx context.Context) (bool, string) {
|
||||
if e.leaseCheck != nil {
|
||||
return e.leaseCheck(ctx)
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,12 @@ type ImportHost interface {
|
|||
Attach(ctx context.Context, info DeviceInfoTruncated, conn net.Conn) (AttachedSession, error)
|
||||
}
|
||||
|
||||
type ExportLeaseIdentity string
|
||||
|
||||
type Export interface {
|
||||
BusID() string
|
||||
Snapshot(ctx context.Context, busy bool) ExportSnapshot
|
||||
LeaseIdentity() ExportLeaseIdentity
|
||||
LeaseCheck(ctx context.Context) (ok bool, reason string)
|
||||
DeviceInfo(ctx context.Context) (DeviceInfoTruncated, error)
|
||||
NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error)
|
||||
|
|
|
|||
|
|
@ -282,6 +282,10 @@ func (e *darwinExport) BusID() string {
|
|||
return e.busid
|
||||
}
|
||||
|
||||
func (e *darwinExport) LeaseIdentity() ExportLeaseIdentity {
|
||||
return ExportLeaseIdentity(fmt.Sprintf("darwin:%016x", e.registryID))
|
||||
}
|
||||
|
||||
func (e *darwinExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
|
||||
stableID := fmt.Sprintf("darwin-registry:%016x", e.registryID)
|
||||
if e.stale {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -50,6 +51,90 @@ func linuxUSBIPStatusReason(status int) string {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -58,6 +143,13 @@ type linuxExportHost struct {
|
|||
exports map[string]*linuxExport
|
||||
}
|
||||
|
||||
type linuxReconcilePlan struct {
|
||||
toRelease []*linuxExport
|
||||
toStale []string
|
||||
toBind map[string]sysfsDevice
|
||||
released []string
|
||||
}
|
||||
|
||||
func newLinuxExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMatch) *linuxExportHost {
|
||||
return &linuxExportHost{
|
||||
logger: logger,
|
||||
|
|
@ -76,9 +168,7 @@ func (h *linuxExportHost) Close() error {
|
|||
h.exports = make(map[string]*linuxExport)
|
||||
h.access.Unlock()
|
||||
for _, exp := range exports {
|
||||
_, statErr := os.Stat(filepath.Join(sysBusUSBDevices, exp.busid))
|
||||
restore := statErr == nil
|
||||
releaseErr := h.releaseExport(exp, restore)
|
||||
releaseErr := h.releaseExport(exp, exp.shouldRestoreCurrentDevice())
|
||||
if releaseErr != nil {
|
||||
h.logger.Warn("rollback ", exp.busid, ": ", releaseErr)
|
||||
}
|
||||
|
|
@ -155,16 +245,48 @@ const (
|
|||
ueventListenerBackoffMax = 30 * time.Second
|
||||
)
|
||||
|
||||
func classifyLinuxReconcile(current map[string]*linuxExport, desired map[string]sysfsDevice, isBusy 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]
|
||||
busy := isBusy(busid)
|
||||
identityMatches := wanted && exp.identity.Equal(newLinuxExportIdentity(device))
|
||||
switch {
|
||||
case exp.stale:
|
||||
if busy {
|
||||
delete(remainingDesired, busid)
|
||||
continue
|
||||
}
|
||||
plan.toRelease = append(plan.toRelease, exp)
|
||||
plan.released = append(plan.released, busid)
|
||||
case identityMatches:
|
||||
delete(remainingDesired, busid)
|
||||
case busy:
|
||||
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 isBusy(busid) {
|
||||
continue
|
||||
}
|
||||
plan.toBind[busid] = device
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
func (h *linuxExportHost) Reconcile(ctx context.Context, isBusy 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")
|
||||
}
|
||||
desired := make(map[string]sysfsDevice)
|
||||
present := make(map[string]struct{}, len(devices))
|
||||
for i := range devices {
|
||||
present[devices[i].BusID] = struct{}{}
|
||||
}
|
||||
for _, m := range h.matches {
|
||||
for i := range devices {
|
||||
deviceKey := DeviceKey{
|
||||
|
|
@ -201,36 +323,42 @@ func (h *linuxExportHost) Reconcile(ctx context.Context, isBusy func(busid strin
|
|||
maps.Copy(current, h.exports)
|
||||
h.access.Unlock()
|
||||
|
||||
for busid, device := range desired {
|
||||
if _, ok := current[busid]; ok {
|
||||
continue
|
||||
plan := classifyLinuxReconcile(current, desired, isBusy)
|
||||
toAdd := make([]*linuxExport, 0, len(plan.toBind))
|
||||
for _, exp := range plan.toRelease {
|
||||
err := h.releaseExport(exp, false)
|
||||
if err != nil {
|
||||
h.logger.Warn("release ", exp.busid, ": ", err)
|
||||
}
|
||||
}
|
||||
|
||||
for busid, device := range plan.toBind {
|
||||
exp, bindErr := h.bindOne(&device)
|
||||
if bindErr != nil {
|
||||
return h.snapshotSelf(), nil, E.Cause(bindErr, "bind ", busid)
|
||||
}
|
||||
h.access.Lock()
|
||||
h.exports[busid] = exp
|
||||
h.access.Unlock()
|
||||
toAdd = append(toAdd, exp)
|
||||
}
|
||||
|
||||
var released []string
|
||||
for busid, exp := range current {
|
||||
if _, ok := desired[busid]; ok {
|
||||
continue
|
||||
h.access.Lock()
|
||||
for _, busid := range plan.toStale {
|
||||
exp, ok := h.exports[busid]
|
||||
if ok {
|
||||
exp.stale = true
|
||||
}
|
||||
_, restore := present[busid]
|
||||
err := h.releaseExport(exp, restore)
|
||||
if err != nil {
|
||||
h.logger.Warn("release ", busid, ": ", err)
|
||||
}
|
||||
h.access.Lock()
|
||||
delete(h.exports, busid)
|
||||
h.access.Unlock()
|
||||
released = append(released, busid)
|
||||
}
|
||||
for _, exp := range plan.toRelease {
|
||||
currentExport, ok := h.exports[exp.busid]
|
||||
if ok && currentExport == exp {
|
||||
delete(h.exports, exp.busid)
|
||||
}
|
||||
}
|
||||
for _, exp := range toAdd {
|
||||
h.exports[exp.busid] = exp
|
||||
}
|
||||
h.access.Unlock()
|
||||
|
||||
return h.snapshotSelf(), released, nil
|
||||
return h.snapshotSelf(), plan.released, nil
|
||||
}
|
||||
|
||||
func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool, error) {
|
||||
|
|
@ -239,7 +367,23 @@ func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool,
|
|||
h.logger.Debug("release ", busid, " from usbip-host: ", err)
|
||||
}
|
||||
waitForUsbipStatusCleared(ctx, busid)
|
||||
return false, nil
|
||||
h.access.Lock()
|
||||
exp, ok := h.exports[busid]
|
||||
h.access.Unlock()
|
||||
if !ok || !exp.stale {
|
||||
return false, nil
|
||||
}
|
||||
releaseErr := h.releaseExport(exp, false)
|
||||
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 {
|
||||
|
|
@ -247,6 +391,9 @@ func (h *linuxExportHost) snapshotSelf() map[string]Export {
|
|||
defer h.access.Unlock()
|
||||
out := make(map[string]Export, len(h.exports))
|
||||
for busid, exp := range h.exports {
|
||||
if exp.stale {
|
||||
continue
|
||||
}
|
||||
out[busid] = exp
|
||||
}
|
||||
return out
|
||||
|
|
@ -343,7 +490,7 @@ func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
|
|||
return err
|
||||
}
|
||||
if !restore {
|
||||
h.logger.Info("removed export state for disappeared device ", exp.busid)
|
||||
h.logger.Info("removed export state for ", exp.busid)
|
||||
return nil
|
||||
}
|
||||
if exp.originalDriver == "" {
|
||||
|
|
@ -362,6 +509,7 @@ func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, origin
|
|||
return &linuxExport{
|
||||
busid: descriptor.BusID,
|
||||
descriptor: descriptor,
|
||||
identity: newLinuxExportIdentity(descriptor),
|
||||
managed: managed,
|
||||
originalDriver: originalDriver,
|
||||
logger: h.logger,
|
||||
|
|
@ -373,20 +521,39 @@ func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, origin
|
|||
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(ctx context.Context, 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 {
|
||||
|
|
@ -422,6 +589,9 @@ func (e *linuxExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
|
|||
}
|
||||
|
||||
func (e *linuxExport) LeaseCheck(ctx context.Context) (bool, string) {
|
||||
if e.stale {
|
||||
return false, "device replaced"
|
||||
}
|
||||
status, err := readUsbipStatus(e.busid)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
|
|
@ -437,6 +607,9 @@ func (e *linuxExport) DeviceInfo(ctx context.Context) (DeviceInfoTruncated, erro
|
|||
}
|
||||
|
||||
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")
|
||||
|
|
@ -458,6 +631,17 @@ func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (
|
|||
return handoff, nil
|
||||
}
|
||||
|
||||
func (e *linuxExport) shouldRestoreCurrentDevice() bool {
|
||||
if e.originalDriver == "" || e.stale {
|
||||
return false
|
||||
}
|
||||
descriptor, err := readSysfsDevice(e.busid, filepath.Join(sysBusUSBDevices, e.busid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return e.identity.Equal(newLinuxExportIdentity(descriptor))
|
||||
}
|
||||
|
||||
type linuxImportHost struct {
|
||||
logger log.ContextLogger
|
||||
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ func (s *ServerService) handleImportExt(conn net.Conn) bool {
|
|||
s.logger.Debug("read import-ext body: ", err)
|
||||
return false
|
||||
}
|
||||
export, ok, reason := s.ledger.ConsumeLeaseAndReserve(request)
|
||||
export, ok, reason := s.ledger.ConsumeLeaseAndReserve(s.ctx, request)
|
||||
if !ok {
|
||||
s.logger.Info("import-ext rejected (", request.BusID, ": ", reason, ")")
|
||||
_ = WriteOpRepImport(conn, OpRepImportExt, OpStatusError, nil)
|
||||
|
|
|
|||
|
|
@ -529,6 +529,8 @@ func darwinIOReturnToUSBIPStatus(status int32) int32 {
|
|||
return 0
|
||||
}
|
||||
switch status {
|
||||
case int32(C.kIOUSBPipeStalled), int32(C.kUSBHostReturnPipeStalled):
|
||||
return -int32(unix.EPIPE)
|
||||
case int32(C.kIOReturnAborted), int32(C.kIOReturnNotResponding):
|
||||
return -int32(unix.ECONNRESET)
|
||||
case int32(C.kIOReturnNoDevice), int32(C.kIOReturnOffline):
|
||||
|
|
@ -551,6 +553,8 @@ func darwinUSBIPStatusToCIStatus(status int32) int {
|
|||
return int(C.IOUSBHostCIMessageStatusSuccess)
|
||||
}
|
||||
switch -status {
|
||||
case int32(unix.EPIPE):
|
||||
return int(C.IOUSBHostCIMessageStatusStallError)
|
||||
case int32(unix.ETIMEDOUT):
|
||||
return int(C.IOUSBHostCIMessageStatusTimeout)
|
||||
case int32(unix.ENOMEM):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#include <IOUSBHost/IOUSBHost.h>
|
||||
#include <IOKit/usb/IOUSBHostFamilyDefinitions.h>
|
||||
#include <IOKit/usb/USB.h>
|
||||
|
||||
#define BOX_USBHOST_MAX_INTERFACES 255
|
||||
|
||||
|
|
|
|||
|
|
@ -331,6 +331,101 @@ static IOUSBHostInterface *box_interface_for_number(BoxUSBHostDevice *box, uint8
|
|||
return nil;
|
||||
}
|
||||
|
||||
static void box_refresh_interfaces(BoxUSBHostDevice *box) {
|
||||
[box withStateLock:^{
|
||||
box.pipes = [NSMutableDictionary dictionary];
|
||||
box_load_interfaces(box);
|
||||
}];
|
||||
}
|
||||
|
||||
static uint8_t box_request_direction(IOUSBDeviceRequest request) {
|
||||
return request.bmRequestType & kIOUSBDeviceRequestDirectionMask;
|
||||
}
|
||||
|
||||
static uint8_t box_request_type(IOUSBDeviceRequest request) {
|
||||
return request.bmRequestType & kIOUSBDeviceRequestTypeMask;
|
||||
}
|
||||
|
||||
static uint8_t box_request_recipient(IOUSBDeviceRequest request) {
|
||||
return request.bmRequestType & kIOUSBDeviceRequestRecipientMask;
|
||||
}
|
||||
|
||||
static BOOL box_request_is_set_configuration(IOUSBDeviceRequest request) {
|
||||
return box_request_direction(request) == kIOUSBDeviceRequestDirectionOut &&
|
||||
box_request_type(request) == kIOUSBDeviceRequestTypeStandard &&
|
||||
box_request_recipient(request) == kIOUSBDeviceRequestRecipientDevice &&
|
||||
request.bRequest == kIOUSBDeviceRequestSetConfiguration &&
|
||||
request.wIndex == 0 &&
|
||||
request.wLength == 0;
|
||||
}
|
||||
|
||||
static BOOL box_request_is_set_interface(IOUSBDeviceRequest request) {
|
||||
return box_request_direction(request) == kIOUSBDeviceRequestDirectionOut &&
|
||||
box_request_type(request) == kIOUSBDeviceRequestTypeStandard &&
|
||||
box_request_recipient(request) == kIOUSBDeviceRequestRecipientInterface &&
|
||||
request.bRequest == kIOUSBDeviceRequestSetInterface &&
|
||||
request.wLength == 0;
|
||||
}
|
||||
|
||||
static BOOL box_request_is_clear_endpoint_halt(IOUSBDeviceRequest request) {
|
||||
return box_request_direction(request) == kIOUSBDeviceRequestDirectionOut &&
|
||||
box_request_type(request) == kIOUSBDeviceRequestTypeStandard &&
|
||||
box_request_recipient(request) == kIOUSBDeviceRequestRecipientEndpoint &&
|
||||
request.bRequest == kIOUSBDeviceRequestClearFeature &&
|
||||
request.wValue == IOUSBEndpointFeatureSelectorStall &&
|
||||
request.wLength == 0;
|
||||
}
|
||||
|
||||
static BOOL box_handle_set_configuration(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSError **error) {
|
||||
BOOL ok = [box.device configureWithValue:request.wValue matchInterfaces:YES error:error];
|
||||
if (ok) {
|
||||
box_refresh_interfaces(box);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static BOOL box_handle_set_interface(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSError **error) {
|
||||
__block IOUSBHostInterface *interface = nil;
|
||||
[box withStateLock:^{
|
||||
interface = box_interface_for_number(box, request.wIndex & 0xff);
|
||||
}];
|
||||
if (interface == nil) {
|
||||
*error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnNotFound userInfo:nil];
|
||||
return NO;
|
||||
}
|
||||
BOOL ok = [interface selectAlternateSetting:request.wValue error:error];
|
||||
if (ok) {
|
||||
box_refresh_interfaces(box);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static BOOL box_handle_clear_endpoint_halt(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSError **error) {
|
||||
uint8_t endpoint = (uint8_t)(request.wIndex & 0xff);
|
||||
if ((endpoint & kIOUSBEndpointDescriptorNumber) == 0) {
|
||||
return [box.device sendDeviceRequest:request data:nil bytesTransferred:NULL completionTimeout:IOUSBHostDefaultControlCompletionTimeout error:error];
|
||||
}
|
||||
IOUSBHostPipe *pipe = box_pipe_for_endpoint(box, endpoint);
|
||||
if (pipe == nil) {
|
||||
*error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnNotFound userInfo:nil];
|
||||
return NO;
|
||||
}
|
||||
return [pipe clearStallWithError:error];
|
||||
}
|
||||
|
||||
static BOOL box_dispatch_control_request(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSMutableData *payload, NSUInteger *actual, NSError **error) {
|
||||
if (box_request_is_set_configuration(request)) {
|
||||
return box_handle_set_configuration(box, request, error);
|
||||
}
|
||||
if (box_request_is_set_interface(request)) {
|
||||
return box_handle_set_interface(box, request, error);
|
||||
}
|
||||
if (box_request_is_clear_endpoint_halt(request)) {
|
||||
return box_handle_clear_endpoint_halt(box, request, error);
|
||||
}
|
||||
return [box.device sendDeviceRequest:request data:payload bytesTransferred:actual completionTimeout:IOUSBHostDefaultControlCompletionTimeout error:error];
|
||||
}
|
||||
|
||||
bool box_usbhost_copy_devices(box_usbhost_device_list_t *out, char **error_out) {
|
||||
if (out == NULL) {
|
||||
box_set_error_string(error_out, @"IOUSBHost copy devices: missing output");
|
||||
|
|
@ -521,34 +616,7 @@ bool box_usbhost_device_control(box_usbhost_device_t *device, const uint8_t setu
|
|||
}
|
||||
NSError *error = nil;
|
||||
NSUInteger actual = 0;
|
||||
BOOL ok = NO;
|
||||
if (request.bmRequestType == 0 && request.bRequest == kIOUSBDeviceRequestSetConfiguration && request.wIndex == 0 && request.wLength == 0) {
|
||||
ok = [box.device configureWithValue:request.wValue matchInterfaces:YES error:&error];
|
||||
if (ok) {
|
||||
[box withStateLock:^{
|
||||
box.pipes = [NSMutableDictionary dictionary];
|
||||
box_load_interfaces(box);
|
||||
}];
|
||||
}
|
||||
} else if (request.bmRequestType == kIOUSBDeviceRequestRecipientInterface && request.bRequest == kIOUSBDeviceRequestSetInterface && request.wLength == 0) {
|
||||
__block IOUSBHostInterface *interface = nil;
|
||||
[box withStateLock:^{
|
||||
interface = box_interface_for_number(box, request.wIndex & 0xff);
|
||||
}];
|
||||
if (interface == nil) {
|
||||
error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnNotFound userInfo:nil];
|
||||
} else {
|
||||
ok = [interface selectAlternateSetting:request.wValue error:&error];
|
||||
if (ok) {
|
||||
[box withStateLock:^{
|
||||
box.pipes = [NSMutableDictionary dictionary];
|
||||
box_load_interfaces(box);
|
||||
}];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok = [box.device sendDeviceRequest:request data:payload bytesTransferred:&actual completionTimeout:IOUSBHostDefaultControlCompletionTimeout error:&error];
|
||||
}
|
||||
BOOL ok = box_dispatch_control_request(box, request, payload, &actual, &error);
|
||||
if (actual_out != NULL) {
|
||||
*actual_out = actual;
|
||||
}
|
||||
|
|
|
|||
32
service/usbip/usbhost_darwin_status_test.go
Normal file
32
service/usbip/usbhost_darwin_status_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//go:build darwin && cgo
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
// From IOKit/usb/USB.h: kIOUSBPipeStalled
|
||||
darwinIOReturnPipeStalled int32 = -536854449
|
||||
// From IOKit/usb/IOUSBHostFamilyDefinitions.h: kUSBHostReturnPipeStalled
|
||||
darwinUSBHostReturnPipeStalled int32 = -536850432
|
||||
)
|
||||
|
||||
func TestDarwinIOReturnToUSBIPStatusMapsStall(t *testing.T) {
|
||||
testCases := []int32{
|
||||
darwinIOReturnPipeStalled,
|
||||
darwinUSBHostReturnPipeStalled,
|
||||
}
|
||||
|
||||
for _, status := range testCases {
|
||||
require.Equal(t, -int32(unix.EPIPE), darwinIOReturnToUSBIPStatus(status))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinUSBIPStatusToCIStatusMapsStall(t *testing.T) {
|
||||
require.Equal(t, ciStatusStallError, darwinUSBIPStatusToCIStatus(-int32(unix.EPIPE)))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue