Files
sing-box/service/usbip/server_darwin_test.go
T
2026-06-09 10:42:26 +08:00

431 lines
12 KiB
Go

//go:build darwin && cgo
package usbip
import (
"context"
"net"
"testing"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/stretchr/testify/require"
)
func TestDarwinServerPendingSubmitUnlinkState(t *testing.T) {
t.Parallel()
session := &darwinServerDataSession{
pending: make(map[uint32]darwinServerPendingSubmit),
}
const endpoint uint8 = 0x81
session.trackSubmit(7, endpoint)
unlinkedEndpoint, active := session.markSubmitUnlinked(7)
require.True(t, active)
require.Equal(t, endpoint, unlinkedEndpoint)
require.False(t, session.finishSubmit(7))
session.trackSubmit(8, endpoint)
require.True(t, session.finishSubmit(8))
_, active = session.markSubmitUnlinked(8)
require.False(t, active)
}
func TestDarwinServerAbortPendingSubmitsMarksAndAbortsEndpoints(t *testing.T) {
t.Parallel()
device := &fakeDarwinServerDataDevice{}
session := &darwinServerDataSession{
logger: newTestLogger(t),
device: device,
pending: make(map[uint32]darwinServerPendingSubmit),
}
session.trackSubmit(7, 0x81)
session.trackSubmit(8, 0x81)
session.trackSubmit(9, 0x02)
session.abortPendingSubmits()
require.Equal(t, []uint8{0x02, 0x81}, device.aborted)
require.False(t, session.finishSubmit(7))
require.False(t, session.finishSubmit(8))
require.False(t, session.finishSubmit(9))
}
func TestDarwinServerServeAbortsPendingSubmitOnClose(t *testing.T) {
t.Parallel()
serverConn, clientConn := net.Pipe()
device := &fakeDarwinServerDataDevice{
ioStarted: make(chan struct{}),
abortNotify: make(chan struct{}),
}
session := newDarwinServerDataSession(context.Background(), newTestLogger(t), serverConn, device)
done := make(chan error, 1)
go func() {
done <- session.serve()
}()
require.NoError(t, WriteSubmitCommand(clientConn, SubmitCommand{
Header: DataHeader{
Command: CmdSubmit,
SeqNum: 1,
Direction: USBIPDirIn,
Endpoint: 1,
},
TransferBufferLength: 8,
}))
select {
case <-device.ioStarted:
case <-time.After(time.Second):
t.Fatal("timed out waiting for pending Darwin IO")
}
require.NoError(t, clientConn.Close())
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timed out waiting for Darwin session shutdown")
}
require.Equal(t, []uint8{0x81}, device.aborted)
}
type fakeDarwinServerDataDevice struct {
ioStarted chan struct{}
abortNotify chan struct{}
aborted []uint8
}
func (d *fakeDarwinServerDataDevice) control(setup [8]byte, buffer []byte) (int32, int32, []byte, error) {
return 0, 0, buffer, nil
}
func (d *fakeDarwinServerDataDevice) io(endpoint uint8, buffer []byte) (int32, int32, []byte, error) {
if d.ioStarted != nil {
close(d.ioStarted)
}
if d.abortNotify != nil {
<-d.abortNotify
}
return usbipStatusECONNRESET, 0, buffer, nil
}
func (d *fakeDarwinServerDataDevice) iso(endpoint uint8, buffer []byte, startFrame int32, packets []IsoPacketDescriptor) (int32, int32, []byte, []IsoPacketDescriptor, error) {
return 0, 0, buffer, packets, nil
}
func (d *fakeDarwinServerDataDevice) abortEndpoint(endpoint uint8) error {
d.aborted = append(d.aborted, endpoint)
if d.abortNotify != nil {
close(d.abortNotify)
}
return nil
}
func TestDarwinServerReconcileAndBroadcastSkipsAfterCancel(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
cancel()
server := &ServerService{
ctx: ctx,
logger: newTestLogger(t),
exports: make(map[string]serverExport),
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
}
require.NoError(t, server.reconcileAndBroadcast(true))
}
func TestDarwinServerUSBEventWatcherTriggersReconcile(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const busid = "mac-00000001"
entry := standardTestDeviceEntry(busid)
info := darwinUSBHostDeviceInfo{
registryID: 1,
entry: entry,
key: DeviceKey{
BusID: busid,
VendorID: entry.Info.IDVendor,
ProductID: entry.Info.IDProduct,
Serial: entry.Serial,
},
}
var devices []darwinUSBHostDeviceInfo
var fakeWatch *fakeDarwinUSBHostDeviceWatch
server := &ServerService{
ctx: ctx,
logger: newTestLogger(t),
matches: []option.USBIPDeviceMatch{{BusID: busid}},
exports: make(map[string]serverExport),
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
ops: darwinServerOps{
copyUSBHostDevices: func() ([]darwinUSBHostDeviceInfo, error) {
return devices, nil
},
openUSBHostDevice: func(registryID uint64, capture bool) (*darwinUSBHostDevice, error) {
require.Equal(t, info.registryID, registryID)
require.True(t, capture)
return &darwinUSBHostDevice{info: info}, nil
},
watchUSBHostDevices: func(callback func()) (darwinUSBHostDeviceWatch, error) {
fakeWatch = &fakeDarwinUSBHostDeviceWatch{callback: callback}
return fakeWatch, nil
},
},
}
watcher, err := server.newUSBEventWatcher()
require.NoError(t, err)
require.NotNil(t, watcher)
require.NotNil(t, fakeWatch)
devices = []darwinUSBHostDeviceInfo{info}
fakeWatch.trigger()
require.Eventually(t, func() bool {
_, ok := server.snapshotExports()[busid]
return ok && darwinServerControlState(server, busid) == deviceStateAvailable
}, time.Second, 10*time.Millisecond)
watcher.Close()
require.True(t, fakeWatch.closed)
}
func TestDarwinServerBuildDeviceStateIncludesBusyExports(t *testing.T) {
t.Parallel()
available := standardTestDeviceEntry("available")
busy := standardTestDeviceEntry("busy")
server := &ServerService{
exports: map[string]serverExport{
"available": {
busid: "available",
registryID: 1,
entry: available,
},
"busy": {
busid: "busy",
registryID: 2,
entry: busy,
busy: true,
},
},
}
devices := deviceInfoV2Map(server.buildDeviceStateV2())
require.Equal(t, deviceStateAvailable, devices["available"].State)
require.Equal(t, deviceStateBusy, devices["busy"].State)
}
func TestDarwinServerReconcileMarksBusyMissingExportStale(t *testing.T) {
t.Parallel()
const busid = "mac-00000001"
entry := standardTestDeviceEntry(busid)
export := serverExport{
busid: busid,
registryID: 1,
device: &darwinUSBHostDevice{},
entry: entry,
busy: true,
}
server := &ServerService{
ctx: context.Background(),
logger: newTestLogger(t),
matches: []option.USBIPDeviceMatch{{BusID: busid}},
exports: map[string]serverExport{busid: export},
controlSubs: make(map[uint64]*serverControlConn),
controlState: deviceInfoV2Map([]DeviceInfoV2{
deviceInfoV2FromEntry(entry, backendIDDarwinIOKit, darwinStableID(export.registryID), deviceStateBusy, 0, deviceStateBusy),
}),
ops: darwinServerOps{
copyUSBHostDevices: func() ([]darwinUSBHostDeviceInfo, error) {
return nil, nil
},
},
}
require.NoError(t, server.reconcileAndBroadcast(true))
snapshot := server.snapshotExports()
require.True(t, snapshot[busid].stale)
require.Equal(t, "", darwinServerControlState(server, busid))
require.True(t, server.releaseClaim(export))
require.NotContains(t, server.snapshotExports(), busid)
}
func TestDarwinServerReconcileCapturesReplacementAfterStaleRelease(t *testing.T) {
t.Parallel()
const busid = "mac-00000001"
oldEntry := standardTestDeviceEntry(busid)
oldExport := serverExport{
busid: busid,
registryID: 1,
device: &darwinUSBHostDevice{},
entry: oldEntry,
busy: true,
}
replacementEntry := standardTestDeviceEntry(busid)
replacementEntry.Info.DevNum = 2
replacementInfo := darwinTestDeviceInfo(2, replacementEntry)
opened := 0
server := &ServerService{
ctx: context.Background(),
logger: newTestLogger(t),
matches: []option.USBIPDeviceMatch{{BusID: busid}},
exports: map[string]serverExport{busid: oldExport},
controlSubs: make(map[uint64]*serverControlConn),
controlState: deviceInfoV2Map([]DeviceInfoV2{
deviceInfoV2FromEntry(oldEntry, backendIDDarwinIOKit, darwinStableID(oldExport.registryID), deviceStateBusy, 0, deviceStateBusy),
}),
ops: darwinServerOps{
copyUSBHostDevices: func() ([]darwinUSBHostDeviceInfo, error) {
return []darwinUSBHostDeviceInfo{replacementInfo}, nil
},
openUSBHostDevice: func(registryID uint64, capture bool) (*darwinUSBHostDevice, error) {
require.Equal(t, replacementInfo.registryID, registryID)
require.True(t, capture)
opened++
return &darwinUSBHostDevice{info: replacementInfo}, nil
},
},
}
require.NoError(t, server.reconcileAndBroadcast(true))
snapshot := server.snapshotExports()
require.True(t, snapshot[busid].stale)
require.Zero(t, opened)
require.Equal(t, "", darwinServerControlState(server, busid))
require.True(t, server.releaseClaim(oldExport))
require.NoError(t, server.reconcileAndBroadcast(true))
snapshot = server.snapshotExports()
require.Equal(t, uint64(2), snapshot[busid].registryID)
require.False(t, snapshot[busid].busy)
require.False(t, snapshot[busid].stale)
require.Equal(t, 1, opened)
require.Equal(t, deviceStateAvailable, darwinServerControlState(server, busid))
}
func TestDarwinServerRegisterControlConnQueuesSnapshotBeforeBroadcast(t *testing.T) {
t.Parallel()
server := &ServerService{
logger: newTestLogger(t),
exports: make(map[string]serverExport),
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
}
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
sub, seq := server.registerControlConn(serverConn, controlCapabilities)
require.Zero(t, seq)
require.Contains(t, server.controlSubs, sub.id)
added := standardTestDeviceEntry("added")
server.exports["added"] = serverExport{busid: "added", registryID: 1, entry: added}
server.broadcastChanged()
first := <-sub.send
require.Equal(t, controlFrameDeviceSnapshot, first.Frame.Type)
require.Zero(t, first.Frame.Sequence)
second := <-sub.send
require.Equal(t, controlFrameDeviceDelta, second.Frame.Type)
require.Equal(t, uint64(1), second.Frame.Sequence)
}
func TestDarwinServerImportBroadcastsBusyState(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const busid = "1-1"
entry := standardTestDeviceEntry(busid)
server := &ServerService{
ctx: ctx,
logger: log.NewNOPFactory().NewLogger("usbip"),
exports: map[string]serverExport{busid: {busid: busid, registryID: 1, entry: entry}},
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
}
serverConn, clientConn := net.Pipe()
done := make(chan struct{})
go func() {
defer close(done)
server.handleImportBusID(serverConn, busid, false)
}()
header, err := ReadOpHeader(clientConn)
require.NoError(t, err)
require.Equal(t, OpRepImport, header.Code)
require.Equal(t, OpStatusOK, header.Status)
_, err = ReadOpRepImportBody(clientConn)
require.NoError(t, err)
require.Eventually(t, func() bool {
return darwinServerControlState(server, busid) == deviceStateBusy
}, time.Second, 10*time.Millisecond)
require.NoError(t, clientConn.Close())
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timed out waiting for Darwin import session shutdown")
}
require.Eventually(t, func() bool {
return darwinServerControlState(server, busid) == deviceStateAvailable
}, time.Second, 10*time.Millisecond)
}
func darwinServerControlState(server *ServerService, busid string) string {
server.controlAccess.Lock()
defer server.controlAccess.Unlock()
return server.controlState[busid].State
}
func darwinTestDeviceInfo(registryID uint64, entry DeviceEntry) darwinUSBHostDeviceInfo {
busid := entry.Info.BusIDString()
return darwinUSBHostDeviceInfo{
registryID: registryID,
entry: entry,
key: DeviceKey{
BusID: busid,
VendorID: entry.Info.IDVendor,
ProductID: entry.Info.IDProduct,
Serial: entrySerial(entry),
},
}
}
type fakeDarwinUSBHostDeviceWatch struct {
callback func()
closed bool
}
func (w *fakeDarwinUSBHostDeviceWatch) Close() {
w.closed = true
}
func (w *fakeDarwinUSBHostDeviceWatch) trigger() {
w.callback()
}