usbip: extract URBEngine + userspaceURBSession from darwin backend

Both the existing darwin backend and the upcoming windows backend
drive USB devices from user space (IOUSBHost CGO calls vs. VBoxUSB
IOCTLs). Refactor the per-attachment URB loop out of host_darwin.go
into a platform-agnostic userspaceURBSession that talks to a URBEngine
interface; the darwin-specific dispatch becomes a 30-line
darwinIOUSBHostEngine. Linux's kernelHandoffSession is untouched.

Move hex8 into shared.go and add usbipStatusEIO so the shared session
does not depend on golang.org/x/sys/unix (Windows has no equivalent).
This commit is contained in:
世界
2026-05-18 12:38:47 +08:00
parent e59e14d89d
commit 6a4ae5263e
24 changed files with 2110 additions and 467 deletions
+1
View File
@@ -26,6 +26,7 @@ const (
maxUSBIPIsoPackets = 1024
nonIsoPacketCount = -1
usbipTransferFlagIsoASAP = 0x0002
usbipStatusEIO = -5
usbipStatusECONNRESET = -104
)
+39
View File
@@ -0,0 +1,39 @@
//go:build darwin && cgo
package usbip
// darwinIOUSBHostEngine drives one captured IOUSBHost device for the
// duration of one USBIP attachment. The device handle itself is owned
// by darwinExportHost (kept across attachments for re-capture), so
// Close here is intentionally a no-op.
type darwinIOUSBHostEngine struct {
device *darwinUSBHostDevice
}
func newDarwinIOUSBHostEngine(device *darwinUSBHostDevice) *darwinIOUSBHostEngine {
return &darwinIOUSBHostEngine{device: device}
}
func (e *darwinIOUSBHostEngine) Submit(req URBRequest) URBResponse {
command := req.Command
switch {
case command.Header.Endpoint == 0:
status, actual, outBuf, err := e.device.control(command.Setup, req.Buffer)
return URBResponse{Status: status, ActualLength: actual, Buffer: outBuf, Error: err}
case command.NumberOfPackets > 0:
asap := command.TransferFlags&usbipTransferFlagIsoASAP != 0
status, actual, outBuf, isoOut, err := e.device.iso(req.Endpoint, req.Buffer, command.StartFrame, asap, req.IsoPackets)
return URBResponse{Status: status, ActualLength: actual, Buffer: outBuf, IsoPackets: isoOut, Error: err}
default:
status, actual, outBuf, err := e.device.io(req.Endpoint, req.Buffer)
return URBResponse{Status: status, ActualLength: actual, Buffer: outBuf, Error: err}
}
}
func (e *darwinIOUSBHostEngine) AbortEndpoint(endpoint uint8) error {
return e.device.abortEndpoint(endpoint)
}
func (e *darwinIOUSBHostEngine) Close() error {
return nil
}
+1 -459
View File
@@ -4,9 +4,7 @@ package usbip
import (
"context"
"errors"
"fmt"
"io"
"maps"
"net"
"slices"
@@ -14,10 +12,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
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) {
@@ -348,7 +343,7 @@ func (e *darwinExport) NewServerDataSession(ctx context.Context, conn net.Conn)
if e.device == nil {
return nil, E.New("darwin export ", e.busid, " has no device handle")
}
return newDarwinServerDataSession(ctx, e.logger, conn, e.device), nil
return newUserspaceURBSession(ctx, e.logger, conn, newDarwinIOUSBHostEngine(e.device)), nil
}
type darwinImportHost struct {
@@ -372,456 +367,3 @@ func (h *darwinImportHost) Attach(ctx context.Context, info DeviceInfoTruncated,
}
return controller, nil
}
var _ DataSession = (*darwinServerDataSession)(nil)
type darwinServerDataSession struct {
ctx context.Context
logger log.ContextLogger
conn net.Conn
device *darwinUSBHostDevice
writeAccess sync.Mutex
access sync.Mutex
pending map[uint32]darwinServerSubmitState
endpoints map[uint8]*darwinServerEndpointState
wg sync.WaitGroup
done chan struct{}
doneOnce sync.Once
runErr error
stateAccess sync.Mutex
started bool
closed bool
closeOnce sync.Once
closeErr error
}
type darwinServerSubmitState struct {
command SubmitCommand
endpoint uint8
started bool
unlinked bool
drained chan struct{}
}
type darwinServerEndpointState struct {
active uint32
queued []uint32
}
type darwinServerNextSubmit struct {
sequence uint32
command SubmitCommand
}
func newDarwinServerDataSession(ctx context.Context, logger log.ContextLogger, conn net.Conn, device *darwinUSBHostDevice) *darwinServerDataSession {
return &darwinServerDataSession{
ctx: ctx,
logger: logger,
conn: conn,
device: device,
pending: make(map[uint32]darwinServerSubmitState),
endpoints: make(map[uint8]*darwinServerEndpointState),
done: make(chan struct{}),
}
}
func (s *darwinServerDataSession) Done() <-chan struct{} {
return s.done
}
func (s *darwinServerDataSession) Err() error {
return s.runErr
}
func (s *darwinServerDataSession) Start() error {
s.stateAccess.Lock()
defer s.stateAccess.Unlock()
if s.started || s.closed {
return nil
}
s.started = true
go s.run()
return nil
}
func (s *darwinServerDataSession) Close() error {
s.closeOnce.Do(func() {
s.closeErr = common.Close(s.conn)
})
s.stateAccess.Lock()
started := s.started
s.closed = true
s.stateAccess.Unlock()
if started {
<-s.done
} else {
s.markDone(nil)
}
return s.closeErr
}
func (s *darwinServerDataSession) markDone(err error) {
s.doneOnce.Do(func() {
s.runErr = err
close(s.done)
})
}
func (s *darwinServerDataSession) run() {
err := s.serve()
if err != nil && (errors.Is(err, io.EOF) || E.IsClosedOrCanceled(err)) {
err = nil
}
s.markDone(err)
}
func (s *darwinServerDataSession) serve() error {
stopCloseOnCancel := closeConnOnContextDone(s.ctx, s.conn)
defer stopCloseOnCancel()
defer func() {
s.abortPendingSubmits()
s.wg.Wait()
}()
for {
header, err := ReadDataHeader(s.conn)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
switch header.Command {
case CmdSubmit:
command, err := ReadSubmitCommandBody(s.conn, header)
if err != nil {
return err
}
next, shouldStart := s.enqueueSubmit(command)
if shouldStart {
s.startSubmit(next)
}
case CmdUnlink:
command, err := ReadUnlinkCommandBody(s.conn, header)
if err != nil {
return err
}
status := int32(0)
endpoint, drained, shouldAbort, found := s.unlinkSubmit(command.SeqNum)
if found {
if shouldAbort {
abortErr := s.device.abortEndpoint(endpoint)
if abortErr != nil {
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", abortErr)
}
}
<-drained
status = usbipStatusECONNRESET
}
s.writeAccess.Lock()
err = WriteUnlinkResponse(s.conn, UnlinkResponse{
Header: DataHeader{Command: RetUnlink, SeqNum: header.SeqNum, DevID: header.DevID, Direction: header.Direction, Endpoint: header.Endpoint},
Status: status,
})
s.writeAccess.Unlock()
if err != nil {
return err
}
default:
return E.New("unexpected USB/IP command ", fmt.Sprintf("0x%08x", header.Command))
}
}
}
func (s *darwinServerDataSession) enqueueSubmit(command SubmitCommand) (darwinServerNextSubmit, bool) {
endpoint := submitScheduleEndpoint(command)
sequence := command.Header.SeqNum
s.access.Lock()
defer s.access.Unlock()
state := darwinServerSubmitState{
command: command,
endpoint: endpoint,
}
endpointState, found := s.endpoints[endpoint]
if !found {
endpointState = &darwinServerEndpointState{}
s.endpoints[endpoint] = endpointState
}
if endpointState.active == 0 {
state.started = true
s.pending[sequence] = state
endpointState.active = sequence
return darwinServerNextSubmit{
sequence: sequence,
command: command,
}, true
}
s.pending[sequence] = state
endpointState.queued = append(endpointState.queued, sequence)
return darwinServerNextSubmit{}, false
}
func (s *darwinServerDataSession) startSubmit(next darwinServerNextSubmit) {
s.wg.Add(1)
go func() {
defer s.wg.Done()
response := s.handleSubmit(next.command)
shouldSend, followUp, hasFollowUp := s.finishSubmit(next.sequence)
if shouldSend {
s.writeAccess.Lock()
err := WriteSubmitResponse(s.conn, response)
s.writeAccess.Unlock()
if err != nil {
_ = s.conn.Close()
}
}
if hasFollowUp {
s.startSubmit(followUp)
}
}()
}
func (s *darwinServerDataSession) handleSubmit(command SubmitCommand) SubmitResponse {
response := SubmitResponse{
Header: DataHeader{
Command: RetSubmit,
SeqNum: command.Header.SeqNum,
DevID: command.Header.DevID,
Direction: command.Header.Direction,
Endpoint: command.Header.Endpoint,
},
StartFrame: command.StartFrame,
NumberOfPackets: command.NumberOfPackets,
IsoPackets: slices.Clone(command.IsoPackets),
}
buffer := command.Buffer
if command.Header.Direction == USBIPDirIn && command.TransferBufferLength > 0 {
buffer = make([]byte, int(command.TransferBufferLength))
}
var (
status int32
actual int32
err error
)
endpoint := commandEndpoint(command)
switch {
case command.Header.Endpoint == 0:
status, actual, buffer, err = s.device.control(command.Setup, buffer)
case command.NumberOfPackets > 0:
asap := command.TransferFlags&usbipTransferFlagIsoASAP != 0
status, actual, buffer, response.IsoPackets, err = s.device.iso(endpoint, buffer, command.StartFrame, asap, response.IsoPackets)
default:
status, actual, buffer, err = s.device.io(endpoint, buffer)
}
if err != nil {
s.logger.Debug("submit seq ", command.Header.SeqNum, " endpoint 0x", hex8(endpoint), ": ", err)
response.Status = -int32(unix.EIO)
return response
}
response.Status = status
if actual < 0 {
actual = 0
}
response.ActualLength = actual
if command.Header.Direction == USBIPDirIn && actual > 0 {
if command.NumberOfPackets > 0 {
response.Buffer = packIsoInResponseBuffer(buffer, response.IsoPackets)
response.ActualLength = int32(len(response.Buffer))
} else {
response.Buffer = buffer[:min(int(actual), len(buffer))]
}
}
return response
}
func packIsoInResponseBuffer(buffer []byte, packets []IsoPacketDescriptor) []byte {
var total int
for i := range packets {
length := int(packets[i].ActualLength)
if length <= 0 {
packets[i].ActualLength = 0
continue
}
offset := int(packets[i].Offset)
if offset < 0 || offset >= len(buffer) {
packets[i].ActualLength = 0
continue
}
if offset+length > len(buffer) {
length = len(buffer) - offset
packets[i].ActualLength = int32(length)
}
total += length
}
if total == 0 {
return nil
}
packed := make([]byte, 0, total)
for i := range packets {
length := int(packets[i].ActualLength)
if length <= 0 {
continue
}
offset := int(packets[i].Offset)
packed = append(packed, buffer[offset:offset+length]...)
}
return packed
}
func (s *darwinServerDataSession) unlinkSubmit(seq uint32) (uint8, <-chan struct{}, bool, bool) {
var drained chan struct{}
s.access.Lock()
pending, found := s.pending[seq]
if !found {
s.access.Unlock()
return 0, nil, false, false
}
if pending.drained == nil {
pending.drained = make(chan struct{})
}
drained = pending.drained
if !pending.started {
endpointState := s.endpoints[pending.endpoint]
if endpointState != nil {
endpointState.queued = removeQueuedSequence(endpointState.queued, seq)
if endpointState.active == 0 && len(endpointState.queued) == 0 {
delete(s.endpoints, pending.endpoint)
}
}
delete(s.pending, seq)
s.access.Unlock()
close(drained)
return pending.endpoint, drained, false, true
}
shouldAbort := !pending.unlinked
pending.unlinked = true
s.pending[seq] = pending
s.access.Unlock()
return pending.endpoint, drained, shouldAbort, true
}
func (s *darwinServerDataSession) finishSubmit(seq uint32) (bool, darwinServerNextSubmit, bool) {
var drained chan struct{}
var followUp darwinServerNextSubmit
var hasFollowUp bool
s.access.Lock()
pending, found := s.pending[seq]
if !found {
s.access.Unlock()
return true, darwinServerNextSubmit{}, false
}
endpointState := s.endpoints[pending.endpoint]
if endpointState != nil && endpointState.active == seq {
endpointState.active = 0
}
delete(s.pending, seq)
if endpointState != nil {
for len(endpointState.queued) > 0 {
nextSequence := endpointState.queued[0]
endpointState.queued = endpointState.queued[1:]
nextPending, nextFound := s.pending[nextSequence]
if !nextFound {
continue
}
nextPending.started = true
s.pending[nextSequence] = nextPending
endpointState.active = nextSequence
followUp = darwinServerNextSubmit{
sequence: nextSequence,
command: nextPending.command,
}
hasFollowUp = true
break
}
if endpointState.active == 0 && len(endpointState.queued) == 0 {
delete(s.endpoints, pending.endpoint)
}
}
drained = pending.drained
unlinked := pending.unlinked
s.access.Unlock()
if drained != nil {
close(drained)
}
return !unlinked, followUp, hasFollowUp
}
func (s *darwinServerDataSession) abortPendingSubmits() {
var (
activeEndpoints []uint8
drained []chan struct{}
)
s.access.Lock()
seen := make(map[uint8]struct{})
for seq, pending := range s.pending {
if !pending.started {
delete(s.pending, seq)
if pending.drained != nil {
drained = append(drained, pending.drained)
}
continue
}
if !pending.unlinked {
seen[pending.endpoint] = struct{}{}
}
pending.unlinked = true
s.pending[seq] = pending
}
for endpoint := range s.endpoints {
endpointState := s.endpoints[endpoint]
if endpointState != nil {
endpointState.queued = nil
}
}
s.access.Unlock()
for _, drainedChannel := range drained {
close(drainedChannel)
}
activeEndpoints = make([]uint8, 0, len(seen))
for endpoint := range seen {
activeEndpoints = append(activeEndpoints, endpoint)
}
slices.Sort(activeEndpoints)
if s.device == nil {
return
}
for _, endpoint := range activeEndpoints {
err := s.device.abortEndpoint(endpoint)
if err != nil {
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", err)
}
}
}
func removeQueuedSequence(queue []uint32, sequence uint32) []uint32 {
for index, current := range queue {
if current != sequence {
continue
}
return append(queue[:index], queue[index+1:]...)
}
return queue
}
func submitScheduleEndpoint(command SubmitCommand) uint8 {
if command.Header.Endpoint == 0 {
return 0
}
return commandEndpoint(command)
}
func commandEndpoint(command SubmitCommand) uint8 {
endpoint := uint8(command.Header.Endpoint & 0x0f)
if command.Header.Direction == USBIPDirIn {
endpoint |= 0x80
}
return endpoint
}
+470
View File
@@ -0,0 +1,470 @@
//go:build linux || (darwin && cgo)
package usbip
import (
"context"
"errors"
"fmt"
"io"
"net"
"slices"
"sync"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ DataSession = (*userspaceURBSession)(nil)
// userspaceURBSession drives one USBIP attachment when the per-URB I/O
// happens in user space (Darwin IOUSBHost, Windows VBoxUSB). It reads
// CMD_SUBMIT/CMD_UNLINK from conn, serializes per endpoint, dispatches
// to a URBEngine, frames RET_SUBMIT/RET_UNLINK back over conn. Linux
// kernel-handoff sessions use kernelHandoffSession instead.
type userspaceURBSession struct {
ctx context.Context
logger log.ContextLogger
conn net.Conn
engine URBEngine
writeAccess sync.Mutex
access sync.Mutex
pending map[uint32]userspaceSubmitState
endpoints map[uint8]*userspaceEndpointState
wg sync.WaitGroup
done chan struct{}
doneOnce sync.Once
runErr error
stateAccess sync.Mutex
started bool
closed bool
closeOnce sync.Once
closeErr error
}
type userspaceSubmitState struct {
command SubmitCommand
endpoint uint8
started bool
unlinked bool
drained chan struct{}
}
type userspaceEndpointState struct {
active uint32
queued []uint32
}
type userspaceNextSubmit struct {
sequence uint32
command SubmitCommand
}
func newUserspaceURBSession(ctx context.Context, logger log.ContextLogger, conn net.Conn, engine URBEngine) *userspaceURBSession {
return &userspaceURBSession{
ctx: ctx,
logger: logger,
conn: conn,
engine: engine,
pending: make(map[uint32]userspaceSubmitState),
endpoints: make(map[uint8]*userspaceEndpointState),
done: make(chan struct{}),
}
}
func (s *userspaceURBSession) Done() <-chan struct{} {
return s.done
}
func (s *userspaceURBSession) Err() error {
return s.runErr
}
func (s *userspaceURBSession) Start() error {
s.stateAccess.Lock()
defer s.stateAccess.Unlock()
if s.started || s.closed {
return nil
}
s.started = true
go s.run()
return nil
}
func (s *userspaceURBSession) Close() error {
s.closeOnce.Do(func() {
s.closeErr = common.Close(s.conn)
})
s.stateAccess.Lock()
started := s.started
s.closed = true
s.stateAccess.Unlock()
if started {
<-s.done
} else {
s.markDone(nil)
}
_ = s.engine.Close()
return s.closeErr
}
func (s *userspaceURBSession) markDone(err error) {
s.doneOnce.Do(func() {
s.runErr = err
close(s.done)
})
}
func (s *userspaceURBSession) run() {
err := s.serve()
if err != nil && (errors.Is(err, io.EOF) || E.IsClosedOrCanceled(err)) {
err = nil
}
s.markDone(err)
}
func (s *userspaceURBSession) serve() error {
stopCloseOnCancel := closeConnOnContextDone(s.ctx, s.conn)
defer stopCloseOnCancel()
defer func() {
s.abortPendingSubmits()
s.wg.Wait()
}()
for {
header, err := ReadDataHeader(s.conn)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
switch header.Command {
case CmdSubmit:
command, err := ReadSubmitCommandBody(s.conn, header)
if err != nil {
return err
}
next, shouldStart := s.enqueueSubmit(command)
if shouldStart {
s.startSubmit(next)
}
case CmdUnlink:
command, err := ReadUnlinkCommandBody(s.conn, header)
if err != nil {
return err
}
status := int32(0)
endpoint, drained, shouldAbort, found := s.unlinkSubmit(command.SeqNum)
if found {
if shouldAbort {
abortErr := s.engine.AbortEndpoint(endpoint)
if abortErr != nil {
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", abortErr)
}
}
<-drained
status = usbipStatusECONNRESET
}
s.writeAccess.Lock()
err = WriteUnlinkResponse(s.conn, UnlinkResponse{
Header: DataHeader{Command: RetUnlink, SeqNum: header.SeqNum, DevID: header.DevID, Direction: header.Direction, Endpoint: header.Endpoint},
Status: status,
})
s.writeAccess.Unlock()
if err != nil {
return err
}
default:
return E.New("unexpected USB/IP command ", fmt.Sprintf("0x%08x", header.Command))
}
}
}
func (s *userspaceURBSession) enqueueSubmit(command SubmitCommand) (userspaceNextSubmit, bool) {
endpoint := submitScheduleEndpoint(command)
sequence := command.Header.SeqNum
s.access.Lock()
defer s.access.Unlock()
state := userspaceSubmitState{
command: command,
endpoint: endpoint,
}
endpointState, found := s.endpoints[endpoint]
if !found {
endpointState = &userspaceEndpointState{}
s.endpoints[endpoint] = endpointState
}
if endpointState.active == 0 {
state.started = true
s.pending[sequence] = state
endpointState.active = sequence
return userspaceNextSubmit{
sequence: sequence,
command: command,
}, true
}
s.pending[sequence] = state
endpointState.queued = append(endpointState.queued, sequence)
return userspaceNextSubmit{}, false
}
func (s *userspaceURBSession) startSubmit(next userspaceNextSubmit) {
s.wg.Add(1)
go func() {
defer s.wg.Done()
response := s.handleSubmit(next.command)
shouldSend, followUp, hasFollowUp := s.finishSubmit(next.sequence)
if shouldSend {
s.writeAccess.Lock()
err := WriteSubmitResponse(s.conn, response)
s.writeAccess.Unlock()
if err != nil {
_ = s.conn.Close()
}
}
if hasFollowUp {
s.startSubmit(followUp)
}
}()
}
func (s *userspaceURBSession) handleSubmit(command SubmitCommand) SubmitResponse {
response := SubmitResponse{
Header: DataHeader{
Command: RetSubmit,
SeqNum: command.Header.SeqNum,
DevID: command.Header.DevID,
Direction: command.Header.Direction,
Endpoint: command.Header.Endpoint,
},
StartFrame: command.StartFrame,
NumberOfPackets: command.NumberOfPackets,
IsoPackets: slices.Clone(command.IsoPackets),
}
buffer := command.Buffer
if command.Header.Direction == USBIPDirIn && command.TransferBufferLength > 0 {
buffer = make([]byte, int(command.TransferBufferLength))
}
endpoint := commandEndpoint(command)
result := s.engine.Submit(URBRequest{
Command: command,
Endpoint: endpoint,
Buffer: buffer,
IsoPackets: response.IsoPackets,
})
if result.Error != nil {
s.logger.Debug("submit seq ", command.Header.SeqNum, " endpoint 0x", hex8(endpoint), ": ", result.Error)
response.Status = usbipStatusEIO
return response
}
response.Status = result.Status
if result.IsoPackets != nil {
response.IsoPackets = result.IsoPackets
}
actual := result.ActualLength
if actual < 0 {
actual = 0
}
response.ActualLength = actual
if command.Header.Direction == USBIPDirIn && actual > 0 {
if command.NumberOfPackets > 0 {
response.Buffer = packIsoInResponseBuffer(result.Buffer, response.IsoPackets)
response.ActualLength = int32(len(response.Buffer))
} else {
response.Buffer = result.Buffer[:min(int(actual), len(result.Buffer))]
}
}
return response
}
func packIsoInResponseBuffer(buffer []byte, packets []IsoPacketDescriptor) []byte {
var total int
for i := range packets {
length := int(packets[i].ActualLength)
if length <= 0 {
packets[i].ActualLength = 0
continue
}
offset := int(packets[i].Offset)
if offset < 0 || offset >= len(buffer) {
packets[i].ActualLength = 0
continue
}
if offset+length > len(buffer) {
length = len(buffer) - offset
packets[i].ActualLength = int32(length)
}
total += length
}
if total == 0 {
return nil
}
packed := make([]byte, 0, total)
for i := range packets {
length := int(packets[i].ActualLength)
if length <= 0 {
continue
}
offset := int(packets[i].Offset)
packed = append(packed, buffer[offset:offset+length]...)
}
return packed
}
func (s *userspaceURBSession) unlinkSubmit(seq uint32) (uint8, <-chan struct{}, bool, bool) {
var drained chan struct{}
s.access.Lock()
pending, found := s.pending[seq]
if !found {
s.access.Unlock()
return 0, nil, false, false
}
if pending.drained == nil {
pending.drained = make(chan struct{})
}
drained = pending.drained
if !pending.started {
endpointState := s.endpoints[pending.endpoint]
if endpointState != nil {
endpointState.queued = removeQueuedSequence(endpointState.queued, seq)
if endpointState.active == 0 && len(endpointState.queued) == 0 {
delete(s.endpoints, pending.endpoint)
}
}
delete(s.pending, seq)
s.access.Unlock()
close(drained)
return pending.endpoint, drained, false, true
}
shouldAbort := !pending.unlinked
pending.unlinked = true
s.pending[seq] = pending
s.access.Unlock()
return pending.endpoint, drained, shouldAbort, true
}
func (s *userspaceURBSession) finishSubmit(seq uint32) (bool, userspaceNextSubmit, bool) {
var drained chan struct{}
var followUp userspaceNextSubmit
var hasFollowUp bool
s.access.Lock()
pending, found := s.pending[seq]
if !found {
s.access.Unlock()
return true, userspaceNextSubmit{}, false
}
endpointState := s.endpoints[pending.endpoint]
if endpointState != nil && endpointState.active == seq {
endpointState.active = 0
}
delete(s.pending, seq)
if endpointState != nil {
for len(endpointState.queued) > 0 {
nextSequence := endpointState.queued[0]
endpointState.queued = endpointState.queued[1:]
nextPending, nextFound := s.pending[nextSequence]
if !nextFound {
continue
}
nextPending.started = true
s.pending[nextSequence] = nextPending
endpointState.active = nextSequence
followUp = userspaceNextSubmit{
sequence: nextSequence,
command: nextPending.command,
}
hasFollowUp = true
break
}
if endpointState.active == 0 && len(endpointState.queued) == 0 {
delete(s.endpoints, pending.endpoint)
}
}
drained = pending.drained
unlinked := pending.unlinked
s.access.Unlock()
if drained != nil {
close(drained)
}
return !unlinked, followUp, hasFollowUp
}
func (s *userspaceURBSession) abortPendingSubmits() {
var (
activeEndpoints []uint8
drained []chan struct{}
)
s.access.Lock()
seen := make(map[uint8]struct{})
for seq, pending := range s.pending {
if !pending.started {
delete(s.pending, seq)
if pending.drained != nil {
drained = append(drained, pending.drained)
}
continue
}
if !pending.unlinked {
seen[pending.endpoint] = struct{}{}
}
pending.unlinked = true
s.pending[seq] = pending
}
for endpoint := range s.endpoints {
endpointState := s.endpoints[endpoint]
if endpointState != nil {
endpointState.queued = nil
}
}
s.access.Unlock()
for _, drainedChannel := range drained {
close(drainedChannel)
}
activeEndpoints = make([]uint8, 0, len(seen))
for endpoint := range seen {
activeEndpoints = append(activeEndpoints, endpoint)
}
slices.Sort(activeEndpoints)
for _, endpoint := range activeEndpoints {
err := s.engine.AbortEndpoint(endpoint)
if err != nil {
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", err)
}
}
}
func removeQueuedSequence(queue []uint32, sequence uint32) []uint32 {
for index, current := range queue {
if current != sequence {
continue
}
return append(queue[:index], queue[index+1:]...)
}
return queue
}
func submitScheduleEndpoint(command SubmitCommand) uint8 {
if command.Header.Endpoint == 0 {
return 0
}
return commandEndpoint(command)
}
func commandEndpoint(command SubmitCommand) uint8 {
endpoint := uint8(command.Header.Endpoint & 0x0f)
if command.Header.Direction == USBIPDirIn {
endpoint |= 0x80
}
return endpoint
}
+5
View File
@@ -42,6 +42,11 @@ func closeConnOnContextDone(ctx context.Context, conn net.Conn) func() {
}
}
func hex8(v uint8) string {
const hexdigits = "0123456789abcdef"
return string([]byte{hexdigits[(v>>4)&0xf], hexdigits[v&0xf]})
}
func describeMatch(m option.USBIPDeviceMatch) string {
var parts []string
if m.BusID != "" {
-8
View File
@@ -1,8 +0,0 @@
//go:build darwin && cgo
package usbip
func hex8(v uint8) string {
const hexdigits = "0123456789abcdef"
return string([]byte{hexdigits[(v>>4)&0xf], hexdigits[v&0xf]})
}
+49
View File
@@ -0,0 +1,49 @@
//go:build linux || (darwin && cgo)
package usbip
// URBEngine executes USB Request Blocks against an already-claimed
// device. The session layer (session_userspace.go) handles framing,
// per-endpoint ordering, and unlink bookkeeping; the engine performs
// the per-URB I/O and per-endpoint aborts only.
//
// Submit is called from per-endpoint goroutines; the session never
// issues two Submits concurrently for the same endpoint, so the engine
// does not need its own cross-endpoint serialization.
type URBEngine interface {
Submit(request URBRequest) URBResponse
// AbortEndpoint cancels all in-flight submits on the given raw
// endpoint address (direction bit included). It is invoked once per
// pending sequence at CMD_UNLINK time and once per active endpoint
// at session shutdown.
AbortEndpoint(endpoint uint8) error
// Close releases engine-owned resources. For engines that own the
// underlying device handle (e.g. Windows VBoxUSB), this releases it.
// For engines where the host manages the device handle separately
// (e.g. Darwin IOUSBHost capture), Close may be a no-op. Idempotent.
Close() error
}
// URBRequest carries one decoded CMD_SUBMIT plus session-owned buffers.
// Buffer holds the OUT payload on entry, or a pre-allocated zero buffer
// for IN transfers. IsoPackets is pre-cloned from the wire command so
// the engine may overwrite descriptors in place during iso completion.
type URBRequest struct {
Command SubmitCommand
Endpoint uint8
Buffer []byte
IsoPackets []IsoPacketDescriptor
}
// URBResponse is the engine's verdict on one URB. Status follows USBIP
// convention (negated errno, 0 on success). ActualLength is the number
// of payload bytes valid in Buffer. Error is engine-internal failure
// distinct from a USB-level error: on Error the session emits
// Status = usbipStatusEIO and logs at Debug.
type URBResponse struct {
Status int32
ActualLength int32
Buffer []byte
IsoPackets []IsoPacketDescriptor
Error error
}