cec39eb00c
Collapse ServerService and ClientService to one definition each by hiding Linux vs Darwin behind ExportHost, ImportHost, and Export seams. Along the way, extract three state machines that previously lived as scattered fields on the service structs: - LeaseManager owns its own mutex and closes the availability-vs-insert TOCTOU by checking export busy inside Issue under the same lock as the insert. - DataSession gives the three per-import data-plane implementations (Linux kernel handoff, Darwin server data session, Darwin virtual controller) a uniform Done/Err/Close interface. - clientAssignment encapsulates the matched/import-all target state and exposes ApplyMatched/ApplyAll diffs to ClientService, which keeps worker goroutine lifecycle. Service busy tracking moves off the per-platform serverExport struct onto ServerService.busy, since it follows the lease/import lifecycle rather than physical claim/release. linux_test.go is migrated to construct ServerService and ClientService through the new host interfaces.
177 lines
4.7 KiB
Go
177 lines
4.7 KiB
Go
//go:build linux || (darwin && cgo)
|
|
|
|
package usbip
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type standardTestDialer struct{}
|
|
|
|
func (standardTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
var dialer net.Dialer
|
|
return dialer.DialContext(ctx, network, destination.String())
|
|
}
|
|
|
|
func (standardTestDialer) ListenPacket(context.Context, M.Socksaddr) (net.PacketConn, error) {
|
|
return nil, errors.New("unused")
|
|
}
|
|
|
|
func TestClientStandardSessionPollsDevList(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
|
|
entry := standardTestDeviceEntry("1-1")
|
|
serverErr := make(chan error, 1)
|
|
go func() {
|
|
serverErr <- serveStandardDevLists(listener, [][]DeviceEntry{
|
|
nil,
|
|
{entry},
|
|
})
|
|
}()
|
|
|
|
matches := []option.USBIPDeviceMatch{{BusID: "1-1"}}
|
|
assignment := newClientAssignment(matches)
|
|
target := assignment.Targets()[0]
|
|
worker := &clientAssignedWorker{target: target, updates: make(chan string, 1)}
|
|
client := &ClientService{
|
|
ctx: ctx,
|
|
logger: log.NewNOPFactory().NewLogger("usbip"),
|
|
dialer: standardTestDialer{},
|
|
serverAddr: standardTestSocksaddr(listener.Addr()),
|
|
matches: matches,
|
|
assignment: assignment,
|
|
assignedWorkers: []*clientAssignedWorker{worker},
|
|
allWorkers: make(map[string]*clientBusIDWorker),
|
|
}
|
|
|
|
sessionErr := make(chan error, 1)
|
|
go func() {
|
|
sessionErr <- client.runStandardSessionWithInterval(10 * time.Millisecond)
|
|
}()
|
|
|
|
select {
|
|
case update := <-worker.updates:
|
|
require.Equal(t, "1-1", update)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for standard devlist refresh")
|
|
}
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-sessionErr:
|
|
require.NoError(t, err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for standard session shutdown")
|
|
}
|
|
require.NoError(t, <-serverErr)
|
|
}
|
|
|
|
func TestClientStandardMatchedAssignmentSurvivesHiddenActiveDevice(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
entry := standardTestDeviceEntry("1-1")
|
|
tests := []struct {
|
|
name string
|
|
match option.USBIPDeviceMatch
|
|
target clientTarget
|
|
}{
|
|
{
|
|
name: "fixed busid",
|
|
match: option.USBIPDeviceMatch{BusID: "1-1"},
|
|
target: clientTarget{fixedBusID: "1-1"},
|
|
},
|
|
{
|
|
name: "device key",
|
|
match: option.USBIPDeviceMatch{VendorID: 0x1d6b, ProductID: 0x0002},
|
|
target: clientTarget{match: option.USBIPDeviceMatch{VendorID: 0x1d6b, ProductID: 0x0002}},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assignment := newClientAssignment([]option.USBIPDeviceMatch{test.match})
|
|
assignment.assigned = []string{"1-1"}
|
|
assignment.SetActive("1-1", true)
|
|
|
|
next, prev := assignment.ApplyMatched([]DeviceEntry{entry}, nil)
|
|
require.Equal(t, []string{"1-1"}, next)
|
|
require.Equal(t, []string{"1-1"}, prev)
|
|
|
|
next, prev = assignment.ApplyMatched(nil, nil)
|
|
require.Equal(t, []string{"1-1"}, next)
|
|
require.Equal(t, []string{"1-1"}, prev)
|
|
|
|
assignment.SetActive("1-1", false)
|
|
next, prev = assignment.ApplyMatched(nil, nil)
|
|
require.Equal(t, []string{""}, next)
|
|
require.Equal(t, []string{"1-1"}, prev)
|
|
})
|
|
}
|
|
}
|
|
|
|
func serveStandardDevLists(listener net.Listener, responses [][]DeviceEntry) error {
|
|
for _, entries := range responses {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := handleStandardDevListConn(conn, entries); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleStandardDevListConn(conn net.Conn, entries []DeviceEntry) error {
|
|
defer conn.Close()
|
|
header, err := ReadOpHeader(conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if header.Version != ProtocolVersion || header.Code != OpReqDevList || header.Status != OpStatusOK {
|
|
return fmt.Errorf("unexpected devlist request: version=0x%04x code=0x%04x status=%d", header.Version, header.Code, header.Status)
|
|
}
|
|
return WriteOpRepDevList(conn, entries)
|
|
}
|
|
|
|
func standardTestDeviceEntry(busid string) DeviceEntry {
|
|
var info DeviceInfoTruncated
|
|
copy(info.BusID[:], busid)
|
|
info.BusNum = 1
|
|
info.DevNum = 1
|
|
info.Speed = SpeedHigh
|
|
info.IDVendor = 0x1d6b
|
|
info.IDProduct = 0x0002
|
|
info.BConfigurationValue = 1
|
|
info.BNumConfigurations = 1
|
|
info.BNumInterfaces = 1
|
|
return DeviceEntry{
|
|
Info: info,
|
|
Interfaces: []DeviceInterface{{
|
|
BInterfaceClass: 0xff,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func standardTestSocksaddr(address net.Addr) M.Socksaddr {
|
|
tcpAddr := address.(*net.TCPAddr)
|
|
return M.ParseSocksaddrHostPort(tcpAddr.IP.String(), uint16(tcpAddr.Port))
|
|
}
|