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:
@@ -26,6 +26,7 @@ const (
|
||||
maxUSBIPIsoPackets = 1024
|
||||
nonIsoPacketCount = -1
|
||||
usbipTransferFlagIsoASAP = 0x0002
|
||||
usbipStatusEIO = -5
|
||||
usbipStatusECONNRESET = -104
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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]})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user