Add usbip virtual-device interop tests
This commit is contained in:
@@ -46,6 +46,11 @@ jobs:
|
||||
run: |
|
||||
echo "BUILD_TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)" >> "$GITHUB_ENV"
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "$GITHUB_ENV"
|
||||
- name: Install usbip tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y usbip
|
||||
- name: Test (unix)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: go test -v -exec sudo -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./...
|
||||
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
)
|
||||
|
||||
var errImmediateReconnect = errors.New("usbip control reconnect")
|
||||
var errControlUnsupported = errors.New("usbip control unsupported")
|
||||
|
||||
type clientTarget struct {
|
||||
fixedBusID string
|
||||
@@ -168,7 +169,7 @@ func (c *ClientService) run() {
|
||||
if !immediate && !sleepCtx(c.ctx, clientReconnectDelay) {
|
||||
break
|
||||
}
|
||||
err := c.runControlSession()
|
||||
err := c.runSession()
|
||||
if c.ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
@@ -180,6 +181,15 @@ func (c *ClientService) run() {
|
||||
c.stopAllWorkers()
|
||||
}
|
||||
|
||||
func (c *ClientService) runSession() error {
|
||||
err := c.runControlSession()
|
||||
if errors.Is(err, errControlUnsupported) {
|
||||
c.logger.Info("control channel unsupported by ", c.serverAddr, "; using standard usbip mode")
|
||||
return c.runStandardSession()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ClientService) runControlSession() error {
|
||||
conn, err := c.dialer.DialContext(c.ctx, N.NetworkTCP, c.serverAddr)
|
||||
if err != nil {
|
||||
@@ -192,23 +202,23 @@ func (c *ClientService) runControlSession() error {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
|
||||
_ = conn.SetReadDeadline(time.Now().Add(controlWriteTimeout))
|
||||
if err := WriteControlPreface(conn); err != nil {
|
||||
return E.Cause(err, "write control preface")
|
||||
return E.Cause(errControlUnsupported, "write control preface: ", err)
|
||||
}
|
||||
if err := WriteControlHello(conn); err != nil {
|
||||
return E.Cause(err, "write control hello")
|
||||
return E.Cause(errControlUnsupported, "write control hello: ", err)
|
||||
}
|
||||
ack, err := ReadControlFrame(conn)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read control ack")
|
||||
return E.Cause(errControlUnsupported, "read control ack: ", err)
|
||||
}
|
||||
if ack.Type != controlFrameAck {
|
||||
return E.New("unexpected control ack frame ", ack.Type)
|
||||
return E.Cause(errControlUnsupported, "unexpected control ack frame ", ack.Type)
|
||||
}
|
||||
if ack.Version != controlProtocolVersion {
|
||||
return E.New("unsupported control version ", ack.Version)
|
||||
return E.Cause(errControlUnsupported, "unsupported control version ", ack.Version)
|
||||
}
|
||||
if ack.Capabilities&controlCapabilities != controlCapabilities {
|
||||
return E.New("missing control capabilities 0x", ack.Capabilities)
|
||||
return E.Cause(errControlUnsupported, "missing control capabilities 0x", ack.Capabilities)
|
||||
}
|
||||
_ = conn.SetWriteDeadline(time.Time{})
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
@@ -246,6 +256,14 @@ func (c *ClientService) runControlSession() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClientService) runStandardSession() error {
|
||||
if err := c.syncRemoteState(); err != nil {
|
||||
return E.Cause(err, "initial devlist sync")
|
||||
}
|
||||
<-c.ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientService) controlPingLoop(conn net.Conn, done <-chan struct{}) {
|
||||
ticker := time.NewTicker(controlPingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
//go:build linux
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
testVendorID uint16 = 0x1d6b
|
||||
testACMProductID uint16 = 0x0104
|
||||
testHIDProductID uint16 = 0x0105
|
||||
)
|
||||
|
||||
var testHIDReportDescriptor = []byte{
|
||||
0x06, 0x00, 0xff,
|
||||
0x09, 0x01,
|
||||
0xa1, 0x01,
|
||||
0x15, 0x00,
|
||||
0x26, 0xff, 0x00,
|
||||
0x75, 0x08,
|
||||
0x95, 0x08,
|
||||
0x09, 0x01,
|
||||
0x81, 0x02,
|
||||
0x95, 0x08,
|
||||
0x09, 0x01,
|
||||
0x91, 0x02,
|
||||
0xc0,
|
||||
}
|
||||
|
||||
type testUSBIPTools struct {
|
||||
usbip string
|
||||
usbipd string
|
||||
}
|
||||
|
||||
type testVirtualFunction struct {
|
||||
name string
|
||||
nodePattern string
|
||||
configure func(functionPath string) error
|
||||
}
|
||||
|
||||
type testVirtualGadget struct {
|
||||
path string
|
||||
serial string
|
||||
busid string
|
||||
functions []testVirtualFunction
|
||||
nodes map[string]string
|
||||
closeOnce sync.Once
|
||||
removeOnce sync.Once
|
||||
udcName string
|
||||
}
|
||||
|
||||
type testACMGadget struct {
|
||||
*testVirtualGadget
|
||||
ttyPath string
|
||||
}
|
||||
|
||||
type testHIDGadget struct {
|
||||
*testVirtualGadget
|
||||
hidPath string
|
||||
}
|
||||
|
||||
type rawFile struct {
|
||||
file *os.File
|
||||
state *term.State
|
||||
}
|
||||
|
||||
type readResult struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func requireUSBIPTools(t *testing.T) testUSBIPTools {
|
||||
t.Helper()
|
||||
requireRoot(t)
|
||||
|
||||
usbipPath, usbipErr := exec.LookPath("usbip")
|
||||
usbipdPath, usbipdErr := exec.LookPath("usbipd")
|
||||
if usbipErr != nil || usbipdErr != nil {
|
||||
t.Skip("usbip and usbipd are required")
|
||||
}
|
||||
return testUSBIPTools{
|
||||
usbip: usbipPath,
|
||||
usbipd: usbipdPath,
|
||||
}
|
||||
}
|
||||
|
||||
func loopbackListenAddr() *badoption.Addr {
|
||||
addr := badoption.Addr(netip.MustParseAddr("127.0.0.1"))
|
||||
return &addr
|
||||
}
|
||||
|
||||
func pickFreeTCPPort(t *testing.T) uint16 {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
return uint16(listener.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
func startRealUSBIPServer(t *testing.T, devices []option.USBIPDeviceMatch) (*ServerService, M.Socksaddr) {
|
||||
t.Helper()
|
||||
|
||||
serviceInstance, err := NewServerService(context.Background(), newTestLogger(), "usbip-server-test", option.USBIPServerServiceOptions{
|
||||
ListenOptions: option.ListenOptions{
|
||||
Listen: loopbackListenAddr(),
|
||||
ListenPort: pickFreeTCPPort(t),
|
||||
},
|
||||
Devices: devices,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
server := serviceInstance.(*ServerService)
|
||||
require.NoError(t, server.Start(adapter.StartStateStart))
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
|
||||
return server, M.SocksaddrFromNet(server.listenFD.Addr())
|
||||
}
|
||||
|
||||
func startRealUSBIPClient(t *testing.T, destination M.Socksaddr, devices []option.USBIPDeviceMatch) *ClientService {
|
||||
t.Helper()
|
||||
|
||||
serviceInstance, err := NewClientService(context.Background(), newTestLogger(), "usbip-client-test", option.USBIPClientServiceOptions{
|
||||
ServerOptions: option.ServerOptions{
|
||||
Server: destination.AddrString(),
|
||||
ServerPort: destination.Port,
|
||||
},
|
||||
Devices: devices,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
client := serviceInstance.(*ClientService)
|
||||
require.NoError(t, client.Start(adapter.StartStateStart))
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
func runCommand(t *testing.T, name string, args ...string) string {
|
||||
t.Helper()
|
||||
|
||||
command := exec.Command(name, args...)
|
||||
command.Env = os.Environ()
|
||||
output, err := command.CombinedOutput()
|
||||
require.NoErrorf(t, err, "%s %s\n%s", name, strings.Join(args, " "), string(output))
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func runUSBIP(t *testing.T, tools testUSBIPTools, args ...string) string {
|
||||
t.Helper()
|
||||
return runCommand(t, tools.usbip, args...)
|
||||
}
|
||||
|
||||
func startUSBIPD(t *testing.T, tools testUSBIPTools, port uint16) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
command := exec.CommandContext(ctx, tools.usbipd, "--debug", "--tcp-port", strconv.Itoa(int(port)))
|
||||
var output bytes.Buffer
|
||||
command.Stdout = &output
|
||||
command.Stderr = &output
|
||||
command.Env = os.Environ()
|
||||
require.NoError(t, command.Start())
|
||||
waitForTCPPort(t, port)
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- command.Wait()
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
_ = command.Process.Kill()
|
||||
<-done
|
||||
case <-done:
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func waitForTCPPort(t *testing.T, port uint16) {
|
||||
t.Helper()
|
||||
|
||||
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))
|
||||
require.Eventually(t, func() bool {
|
||||
conn, err := net.DialTimeout("tcp", address, 200*time.Millisecond)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = conn.Close()
|
||||
return true
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func snapshotPaths(pattern string) map[string]struct{} {
|
||||
paths, _ := filepath.Glob(pattern)
|
||||
snapshot := make(map[string]struct{}, len(paths))
|
||||
for _, path := range paths {
|
||||
snapshot[path] = struct{}{}
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func newPaths(pattern string, before map[string]struct{}) []string {
|
||||
paths, _ := filepath.Glob(pattern)
|
||||
var out []string
|
||||
for _, path := range paths {
|
||||
if _, found := before[path]; found {
|
||||
continue
|
||||
}
|
||||
out = append(out, path)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func waitForNewPath(t *testing.T, pattern string, before map[string]struct{}) string {
|
||||
t.Helper()
|
||||
|
||||
var found string
|
||||
require.Eventually(t, func() bool {
|
||||
paths := newPaths(pattern, before)
|
||||
if len(paths) == 0 {
|
||||
return false
|
||||
}
|
||||
found = paths[0]
|
||||
return true
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return found
|
||||
}
|
||||
|
||||
func importedNodeSnapshot(pattern string) map[string]struct{} {
|
||||
paths, _ := filepath.Glob(pattern)
|
||||
snapshot := make(map[string]struct{}, len(paths))
|
||||
for _, path := range paths {
|
||||
if isVHCINode(path) {
|
||||
snapshot[path] = struct{}{}
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func isVHCINode(path string) bool {
|
||||
base := filepath.Base(path)
|
||||
var sysfsPath string
|
||||
switch {
|
||||
case strings.HasPrefix(base, "ttyACM"):
|
||||
sysfsPath = filepath.Join("/sys/class/tty", base, "device")
|
||||
case strings.HasPrefix(base, "hidraw"):
|
||||
sysfsPath = filepath.Join("/sys/class/hidraw", base, "device")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
realPath, err := filepath.EvalSymlinks(sysfsPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(realPath, "vhci_hcd")
|
||||
}
|
||||
|
||||
func waitForNewImportedNode(t *testing.T, pattern string, before map[string]struct{}) string {
|
||||
t.Helper()
|
||||
|
||||
var found string
|
||||
require.Eventually(t, func() bool {
|
||||
paths, _ := filepath.Glob(pattern)
|
||||
var candidates []string
|
||||
for _, path := range paths {
|
||||
if !isVHCINode(path) {
|
||||
continue
|
||||
}
|
||||
if _, present := before[path]; present {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, path)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return false
|
||||
}
|
||||
sort.Strings(candidates)
|
||||
found = candidates[0]
|
||||
return true
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
return found
|
||||
}
|
||||
|
||||
func waitForPathGone(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
require.Eventually(t, func() bool {
|
||||
_, err := os.Stat(path)
|
||||
return os.IsNotExist(err)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func ensureNoNewImportedNode(t *testing.T, pattern string, before map[string]struct{}, duration time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(duration)
|
||||
for time.Now().Before(deadline) {
|
||||
paths, _ := filepath.Glob(pattern)
|
||||
for _, path := range paths {
|
||||
if !isVHCINode(path) {
|
||||
continue
|
||||
}
|
||||
if _, present := before[path]; !present {
|
||||
t.Fatalf("unexpected imported node %s", path)
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func usedVHCIPorts(t *testing.T) map[int]struct{} {
|
||||
t.Helper()
|
||||
|
||||
records, err := readVHCIStatus()
|
||||
require.NoError(t, err)
|
||||
|
||||
ports := make(map[int]struct{})
|
||||
for _, record := range records {
|
||||
if record.state == 6 {
|
||||
ports[record.port] = struct{}{}
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func waitForNewUsedVHCIPort(t *testing.T, before map[int]struct{}) int {
|
||||
t.Helper()
|
||||
|
||||
var port int
|
||||
require.Eventually(t, func() bool {
|
||||
records, err := readVHCIStatus()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, record := range records {
|
||||
if record.state != 6 {
|
||||
continue
|
||||
}
|
||||
if _, found := before[record.port]; found {
|
||||
continue
|
||||
}
|
||||
port = record.port
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
return port
|
||||
}
|
||||
|
||||
func readExactlyAsync(reader io.Reader, size int) <-chan readResult {
|
||||
results := make(chan readResult, 1)
|
||||
go func() {
|
||||
buffer := make([]byte, size)
|
||||
_, err := io.ReadFull(reader, buffer)
|
||||
results <- readResult{
|
||||
data: buffer,
|
||||
err: err,
|
||||
}
|
||||
}()
|
||||
return results
|
||||
}
|
||||
|
||||
func requireRead(t *testing.T, results <-chan readResult, expected []byte) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case result := <-results:
|
||||
require.NoError(t, result.err)
|
||||
require.Equal(t, expected, result.data)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for device I/O")
|
||||
}
|
||||
}
|
||||
|
||||
func openRawTTY(t *testing.T, path string) *rawFile {
|
||||
t.Helper()
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
state, err := term.MakeRaw(int(file.Fd()))
|
||||
require.NoError(t, err)
|
||||
return &rawFile{
|
||||
file: file,
|
||||
state: state,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rawFile) Close() {
|
||||
if r == nil || r.file == nil {
|
||||
return
|
||||
}
|
||||
_ = term.Restore(int(r.file.Fd()), r.state)
|
||||
_ = r.file.Close()
|
||||
}
|
||||
|
||||
func openBinaryDevice(t *testing.T, path string) *os.File {
|
||||
t.Helper()
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
return file
|
||||
}
|
||||
|
||||
func newTestVirtualGadget(t *testing.T, productID uint16, productName string, functions []testVirtualFunction) *testVirtualGadget {
|
||||
t.Helper()
|
||||
requireRoot(t)
|
||||
|
||||
requireKernelModule(t, "configfs")
|
||||
requireKernelModule(t, "libcomposite")
|
||||
requireKernelModule(t, "dummy_hcd")
|
||||
|
||||
udcs, err := os.ReadDir("/sys/class/udc")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, udcs)
|
||||
|
||||
snapshots := make(map[string]map[string]struct{})
|
||||
for _, function := range functions {
|
||||
if function.nodePattern == "" {
|
||||
continue
|
||||
}
|
||||
snapshots[function.name] = snapshotPaths(function.nodePattern)
|
||||
}
|
||||
|
||||
gadget := &testVirtualGadget{
|
||||
path: filepath.Join("/sys/kernel/config/usb_gadget", fmt.Sprintf("codex_usbip_%d", time.Now().UnixNano())),
|
||||
serial: fmt.Sprintf("codex-usbip-%d", time.Now().UnixNano()),
|
||||
functions: functions,
|
||||
nodes: make(map[string]string, len(functions)),
|
||||
udcName: udcs[0].Name(),
|
||||
}
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(gadget.path, "strings/0x409"), 0o755))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(gadget.path, "configs/c.1/strings/0x409"), 0o755))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "idVendor"), fmt.Sprintf("0x%04x", testVendorID)))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "idProduct"), fmt.Sprintf("0x%04x", productID)))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "strings/0x409/serialnumber"), gadget.serial))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "strings/0x409/manufacturer"), "OpenAI"))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "strings/0x409/product"), productName))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "configs/c.1/strings/0x409/configuration"), "config-1"))
|
||||
|
||||
for _, function := range functions {
|
||||
functionPath := filepath.Join(gadget.path, "functions", function.name)
|
||||
require.NoError(t, os.MkdirAll(functionPath, 0o755))
|
||||
if function.configure != nil {
|
||||
require.NoError(t, function.configure(functionPath))
|
||||
}
|
||||
require.NoError(t, os.Symlink(functionPath, filepath.Join(gadget.path, "configs/c.1", function.name)))
|
||||
}
|
||||
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "UDC"), gadget.udcName))
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
devices, err := listUSBDevices()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for i := range devices {
|
||||
if devices[i].VendorID == testVendorID &&
|
||||
devices[i].ProductID == productID &&
|
||||
devices[i].Serial == gadget.serial {
|
||||
gadget.busid = devices[i].BusID
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
for _, function := range functions {
|
||||
if function.nodePattern == "" {
|
||||
continue
|
||||
}
|
||||
gadget.nodes[function.name] = waitForNewPath(t, function.nodePattern, snapshots[function.name])
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
gadget.Close()
|
||||
})
|
||||
|
||||
return gadget
|
||||
}
|
||||
|
||||
func (g *testVirtualGadget) Close() {
|
||||
g.closeOnce.Do(func() {
|
||||
if g.busid != "" {
|
||||
if driver, err := currentDriver(g.busid); err == nil && driver == "usbip-host" {
|
||||
_ = hostUnbind(g.busid)
|
||||
_ = hostMatchBusID(g.busid, false)
|
||||
}
|
||||
}
|
||||
|
||||
_ = writeSysfsLine(filepath.Join(g.path, "UDC"), "")
|
||||
|
||||
for _, function := range g.functions {
|
||||
_ = os.Remove(filepath.Join(g.path, "configs/c.1", function.name))
|
||||
}
|
||||
for _, function := range g.functions {
|
||||
_ = os.RemoveAll(filepath.Join(g.path, "functions", function.name))
|
||||
}
|
||||
_ = os.RemoveAll(filepath.Join(g.path, "configs/c.1/strings/0x409"))
|
||||
_ = os.RemoveAll(filepath.Join(g.path, "configs/c.1"))
|
||||
_ = os.RemoveAll(filepath.Join(g.path, "strings/0x409"))
|
||||
_ = os.RemoveAll(g.path)
|
||||
})
|
||||
}
|
||||
|
||||
func newTestACMGadget(t *testing.T) *testACMGadget {
|
||||
t.Helper()
|
||||
|
||||
gadget := newTestVirtualGadget(t, testACMProductID, "Codex USBIP ACM", []testVirtualFunction{{
|
||||
name: "acm.usb0",
|
||||
nodePattern: "/dev/ttyGS*",
|
||||
}})
|
||||
return &testACMGadget{
|
||||
testVirtualGadget: gadget,
|
||||
ttyPath: gadget.nodes["acm.usb0"],
|
||||
}
|
||||
}
|
||||
|
||||
func newTestHIDGadget(t *testing.T) *testHIDGadget {
|
||||
t.Helper()
|
||||
|
||||
gadget := newTestVirtualGadget(t, testHIDProductID, "Codex USBIP HID", []testVirtualFunction{{
|
||||
name: "hid.usb0",
|
||||
nodePattern: "/dev/hidg*",
|
||||
configure: func(functionPath string) error {
|
||||
if err := writeSysfs(functionPath+"/protocol", "0"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeSysfs(functionPath+"/subclass", "0"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeSysfs(functionPath+"/report_length", "8"); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(functionPath+"/report_desc", testHIDReportDescriptor, 0o644)
|
||||
},
|
||||
}})
|
||||
return &testHIDGadget{
|
||||
testVirtualGadget: gadget,
|
||||
hidPath: gadget.nodes["hid.usb0"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *testACMGadget) exerciseImportedIO(t *testing.T, importedTTY string) {
|
||||
t.Helper()
|
||||
|
||||
gadgetTTY := openRawTTY(t, g.ttyPath)
|
||||
imported := openRawTTY(t, importedTTY)
|
||||
defer gadgetTTY.Close()
|
||||
defer imported.Close()
|
||||
|
||||
gadgetToHost := []byte("acm-g2h!")
|
||||
hostToGadget := []byte("acm-h2g!")
|
||||
|
||||
hostRead := readExactlyAsync(imported.file, len(gadgetToHost))
|
||||
_, err := gadgetTTY.file.Write(gadgetToHost)
|
||||
require.NoError(t, err)
|
||||
requireRead(t, hostRead, gadgetToHost)
|
||||
|
||||
gadgetRead := readExactlyAsync(gadgetTTY.file, len(hostToGadget))
|
||||
_, err = imported.file.Write(hostToGadget)
|
||||
require.NoError(t, err)
|
||||
requireRead(t, gadgetRead, hostToGadget)
|
||||
}
|
||||
|
||||
func (g *testHIDGadget) exerciseImportedIO(t *testing.T, importedHID string) {
|
||||
t.Helper()
|
||||
|
||||
gadgetHID := openBinaryDevice(t, g.hidPath)
|
||||
imported := openBinaryDevice(t, importedHID)
|
||||
defer gadgetHID.Close()
|
||||
defer imported.Close()
|
||||
|
||||
gadgetToHost := []byte{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
hostToGadget := []byte{8, 7, 6, 5, 4, 3, 2, 1}
|
||||
|
||||
hostRead := readExactlyAsync(imported, len(gadgetToHost))
|
||||
_, err := gadgetHID.Write(gadgetToHost)
|
||||
require.NoError(t, err)
|
||||
requireRead(t, hostRead, gadgetToHost)
|
||||
|
||||
gadgetRead := readExactlyAsync(gadgetHID, len(hostToGadget))
|
||||
_, err = imported.Write(hostToGadget)
|
||||
require.NoError(t, err)
|
||||
requireRead(t, gadgetRead, hostToGadget)
|
||||
}
|
||||
|
||||
func bindWithOfficialUSBIP(t *testing.T, tools testUSBIPTools, busid string) {
|
||||
t.Helper()
|
||||
runUSBIP(t, tools, "bind", "--busid="+busid)
|
||||
}
|
||||
|
||||
func unbindWithOfficialUSBIP(t *testing.T, tools testUSBIPTools, busid string) {
|
||||
t.Helper()
|
||||
runUSBIP(t, tools, "unbind", "--busid="+busid)
|
||||
}
|
||||
|
||||
func TestUSBIPInteropOurServerWithOfficialClientACM(t *testing.T) {
|
||||
requireRoot(t)
|
||||
tools := requireUSBIPTools(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
gadget := newTestACMGadget(t)
|
||||
server, address := startRealUSBIPServer(t, []option.USBIPDeviceMatch{{Serial: gadget.serial}})
|
||||
beforePorts := usedVHCIPorts(t)
|
||||
beforeTTY := importedNodeSnapshot("/dev/ttyACM*")
|
||||
|
||||
listOutput := runUSBIP(t, tools, "--tcp-port", strconv.Itoa(int(address.Port)), "list", "--remote=127.0.0.1")
|
||||
require.Contains(t, listOutput, gadget.busid)
|
||||
|
||||
runUSBIP(t, tools, "--tcp-port", strconv.Itoa(int(address.Port)), "attach", "--remote=127.0.0.1", "--busid="+gadget.busid)
|
||||
port := waitForNewUsedVHCIPort(t, beforePorts)
|
||||
importedTTY := waitForNewImportedNode(t, "/dev/ttyACM*", beforeTTY)
|
||||
gadget.exerciseImportedIO(t, importedTTY)
|
||||
|
||||
portOutput := runUSBIP(t, tools, "port")
|
||||
require.Contains(t, portOutput, fmt.Sprintf("Port %02d", port))
|
||||
|
||||
runUSBIP(t, tools, "detach", "--port="+strconv.Itoa(port))
|
||||
waitForPathGone(t, importedTTY)
|
||||
|
||||
_ = server
|
||||
}
|
||||
|
||||
func TestUSBIPInteropOurServerWithOfficialClientHID(t *testing.T) {
|
||||
requireRoot(t)
|
||||
tools := requireUSBIPTools(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
gadget := newTestHIDGadget(t)
|
||||
_, address := startRealUSBIPServer(t, []option.USBIPDeviceMatch{{Serial: gadget.serial}})
|
||||
beforePorts := usedVHCIPorts(t)
|
||||
beforeHID := importedNodeSnapshot("/dev/hidraw*")
|
||||
|
||||
listOutput := runUSBIP(t, tools, "--tcp-port", strconv.Itoa(int(address.Port)), "list", "--remote=127.0.0.1")
|
||||
require.Contains(t, listOutput, gadget.busid)
|
||||
|
||||
runUSBIP(t, tools, "--tcp-port", strconv.Itoa(int(address.Port)), "attach", "--remote=127.0.0.1", "--busid="+gadget.busid)
|
||||
port := waitForNewUsedVHCIPort(t, beforePorts)
|
||||
importedHID := waitForNewImportedNode(t, "/dev/hidraw*", beforeHID)
|
||||
gadget.exerciseImportedIO(t, importedHID)
|
||||
|
||||
portOutput := runUSBIP(t, tools, "port")
|
||||
require.Contains(t, portOutput, fmt.Sprintf("Port %02d", port))
|
||||
|
||||
runUSBIP(t, tools, "detach", "--port="+strconv.Itoa(port))
|
||||
waitForPathGone(t, importedHID)
|
||||
}
|
||||
|
||||
func TestUSBIPInteropOurClientWithOfficialServerACM(t *testing.T) {
|
||||
requireRoot(t)
|
||||
tools := requireUSBIPTools(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
gadget := newTestACMGadget(t)
|
||||
bindWithOfficialUSBIP(t, tools, gadget.busid)
|
||||
t.Cleanup(func() {
|
||||
unbindWithOfficialUSBIP(t, tools, gadget.busid)
|
||||
})
|
||||
|
||||
port := pickFreeTCPPort(t)
|
||||
startUSBIPD(t, tools, port)
|
||||
beforeTTY := importedNodeSnapshot("/dev/ttyACM*")
|
||||
|
||||
client := startRealUSBIPClient(t, M.ParseSocksaddrHostPort("127.0.0.1", port), []option.USBIPDeviceMatch{{
|
||||
VendorID: option.USBIPHexUint16(testVendorID),
|
||||
ProductID: option.USBIPHexUint16(testACMProductID),
|
||||
}})
|
||||
_ = client
|
||||
|
||||
importedTTY := waitForNewImportedNode(t, "/dev/ttyACM*", beforeTTY)
|
||||
gadget.exerciseImportedIO(t, importedTTY)
|
||||
|
||||
require.NoError(t, client.Close())
|
||||
waitForPathGone(t, importedTTY)
|
||||
}
|
||||
|
||||
func TestUSBIPInteropOurClientWithOfficialServerHID(t *testing.T) {
|
||||
requireRoot(t)
|
||||
tools := requireUSBIPTools(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
gadget := newTestHIDGadget(t)
|
||||
bindWithOfficialUSBIP(t, tools, gadget.busid)
|
||||
t.Cleanup(func() {
|
||||
unbindWithOfficialUSBIP(t, tools, gadget.busid)
|
||||
})
|
||||
|
||||
port := pickFreeTCPPort(t)
|
||||
startUSBIPD(t, tools, port)
|
||||
beforeHID := importedNodeSnapshot("/dev/hidraw*")
|
||||
|
||||
client := startRealUSBIPClient(t, M.ParseSocksaddrHostPort("127.0.0.1", port), []option.USBIPDeviceMatch{{
|
||||
VendorID: option.USBIPHexUint16(testVendorID),
|
||||
ProductID: option.USBIPHexUint16(testHIDProductID),
|
||||
}})
|
||||
_ = client
|
||||
|
||||
importedHID := waitForNewImportedNode(t, "/dev/hidraw*", beforeHID)
|
||||
gadget.exerciseImportedIO(t, importedHID)
|
||||
|
||||
require.NoError(t, client.Close())
|
||||
waitForPathGone(t, importedHID)
|
||||
}
|
||||
|
||||
func TestUSBIPOfficialServerHasStaticDiscoveryOnly(t *testing.T) {
|
||||
requireRoot(t)
|
||||
tools := requireUSBIPTools(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
first := newTestACMGadget(t)
|
||||
bindWithOfficialUSBIP(t, tools, first.busid)
|
||||
t.Cleanup(func() {
|
||||
unbindWithOfficialUSBIP(t, tools, first.busid)
|
||||
})
|
||||
|
||||
port := pickFreeTCPPort(t)
|
||||
startUSBIPD(t, tools, port)
|
||||
beforeTTY := importedNodeSnapshot("/dev/ttyACM*")
|
||||
|
||||
client := startRealUSBIPClient(t, M.ParseSocksaddrHostPort("127.0.0.1", port), nil)
|
||||
_ = client
|
||||
|
||||
importedTTY := waitForNewImportedNode(t, "/dev/ttyACM*", beforeTTY)
|
||||
first.exerciseImportedIO(t, importedTTY)
|
||||
|
||||
second := newTestHIDGadget(t)
|
||||
bindWithOfficialUSBIP(t, tools, second.busid)
|
||||
t.Cleanup(func() {
|
||||
unbindWithOfficialUSBIP(t, tools, second.busid)
|
||||
})
|
||||
|
||||
beforeHID := importedNodeSnapshot("/dev/hidraw*")
|
||||
ensureNoNewImportedNode(t, "/dev/hidraw*", beforeHID, 3*time.Second)
|
||||
|
||||
require.NoError(t, client.Close())
|
||||
waitForPathGone(t, importedTTY)
|
||||
}
|
||||
|
||||
func TestUSBIPControlHotplugACMReattach(t *testing.T) {
|
||||
requireRoot(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
_, address := startRealUSBIPServer(t, []option.USBIPDeviceMatch{{
|
||||
VendorID: option.USBIPHexUint16(testVendorID),
|
||||
ProductID: option.USBIPHexUint16(testACMProductID),
|
||||
}})
|
||||
client := startRealUSBIPClient(t, address, []option.USBIPDeviceMatch{{
|
||||
VendorID: option.USBIPHexUint16(testVendorID),
|
||||
ProductID: option.USBIPHexUint16(testACMProductID),
|
||||
}})
|
||||
_ = client
|
||||
|
||||
beforeTTY := importedNodeSnapshot("/dev/ttyACM*")
|
||||
first := newTestACMGadget(t)
|
||||
firstImportedTTY := waitForNewImportedNode(t, "/dev/ttyACM*", beforeTTY)
|
||||
first.exerciseImportedIO(t, firstImportedTTY)
|
||||
|
||||
first.Close()
|
||||
waitForPathGone(t, firstImportedTTY)
|
||||
|
||||
secondBefore := importedNodeSnapshot("/dev/ttyACM*")
|
||||
second := newTestACMGadget(t)
|
||||
secondImportedTTY := waitForNewImportedNode(t, "/dev/ttyACM*", secondBefore)
|
||||
second.exerciseImportedIO(t, secondImportedTTY)
|
||||
}
|
||||
|
||||
func TestUSBIPControlImportAllACMAndHID(t *testing.T) {
|
||||
requireRoot(t)
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
_, address := startRealUSBIPServer(t, []option.USBIPDeviceMatch{
|
||||
{VendorID: option.USBIPHexUint16(testVendorID), ProductID: option.USBIPHexUint16(testACMProductID)},
|
||||
{VendorID: option.USBIPHexUint16(testVendorID), ProductID: option.USBIPHexUint16(testHIDProductID)},
|
||||
})
|
||||
client := startRealUSBIPClient(t, address, nil)
|
||||
_ = client
|
||||
|
||||
beforeTTY := importedNodeSnapshot("/dev/ttyACM*")
|
||||
beforeHID := importedNodeSnapshot("/dev/hidraw*")
|
||||
|
||||
acm := newTestACMGadget(t)
|
||||
hid := newTestHIDGadget(t)
|
||||
|
||||
importedTTY := waitForNewImportedNode(t, "/dev/ttyACM*", beforeTTY)
|
||||
importedHID := waitForNewImportedNode(t, "/dev/hidraw*", beforeHID)
|
||||
|
||||
acm.exerciseImportedIO(t, importedTTY)
|
||||
hid.exerciseImportedIO(t, importedHID)
|
||||
}
|
||||
+167
-11
@@ -5,16 +5,20 @@ package usbip
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -242,6 +246,105 @@ func startDispatchServer(t *testing.T, server *ServerService) (M.Socksaddr, func
|
||||
}
|
||||
}
|
||||
|
||||
type testUSBGadget struct {
|
||||
path string
|
||||
serial string
|
||||
busid string
|
||||
}
|
||||
|
||||
func requireRoot(t *testing.T) {
|
||||
t.Helper()
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("root required")
|
||||
}
|
||||
}
|
||||
|
||||
func requireKernelModule(t *testing.T, module string) {
|
||||
t.Helper()
|
||||
|
||||
modprobePath, err := findModprobePath()
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := shell.Exec(modprobePath, module).Read()
|
||||
require.NoErrorf(t, err, "modprobe %s: %s", module, output)
|
||||
}
|
||||
|
||||
func writeSysfsLine(path string, content string) error {
|
||||
return os.WriteFile(path, []byte(content+"\n"), 0)
|
||||
}
|
||||
|
||||
func newTestUSBGadget(t *testing.T) *testUSBGadget {
|
||||
t.Helper()
|
||||
requireRoot(t)
|
||||
|
||||
requireKernelModule(t, "configfs")
|
||||
requireKernelModule(t, "libcomposite")
|
||||
requireKernelModule(t, "dummy_hcd")
|
||||
|
||||
udcs, err := os.ReadDir("/sys/class/udc")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, udcs)
|
||||
|
||||
gadget := &testUSBGadget{
|
||||
path: filepath.Join("/sys/kernel/config/usb_gadget", fmt.Sprintf("codex_usbip_%d", time.Now().UnixNano())),
|
||||
serial: fmt.Sprintf("codex-usbip-%d", time.Now().UnixNano()),
|
||||
}
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(gadget.path, "strings/0x409"), 0o755))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(gadget.path, "configs/c.1/strings/0x409"), 0o755))
|
||||
require.NoError(t, os.Mkdir(filepath.Join(gadget.path, "functions/acm.usb0"), 0o755))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "idVendor"), "0x1d6b"))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "idProduct"), "0x0104"))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "strings/0x409/serialnumber"), gadget.serial))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "strings/0x409/manufacturer"), "OpenAI"))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "strings/0x409/product"), "Codex USBIP Test"))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "configs/c.1/strings/0x409/configuration"), "config-1"))
|
||||
require.NoError(t, os.Symlink(filepath.Join(gadget.path, "functions/acm.usb0"), filepath.Join(gadget.path, "configs/c.1/acm.usb0")))
|
||||
require.NoError(t, writeSysfs(filepath.Join(gadget.path, "UDC"), udcs[0].Name()))
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
devices, err := listUSBDevices()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for i := range devices {
|
||||
if devices[i].VendorID == 0x1d6b &&
|
||||
devices[i].ProductID == 0x0104 &&
|
||||
devices[i].Serial == gadget.serial {
|
||||
gadget.busid = devices[i].BusID
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
|
||||
t.Cleanup(func() {
|
||||
if gadget.busid != "" {
|
||||
if driver, err := currentDriver(gadget.busid); err == nil {
|
||||
switch driver {
|
||||
case "usbip-host":
|
||||
_ = hostUnbind(gadget.busid)
|
||||
_ = hostMatchBusID(gadget.busid, false)
|
||||
_ = bindToDriver(gadget.busid, "usb")
|
||||
case "usb":
|
||||
case "":
|
||||
default:
|
||||
_ = bindToDriver(gadget.busid, "usb")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = writeSysfsLine(filepath.Join(gadget.path, "UDC"), "")
|
||||
_ = os.Remove(filepath.Join(gadget.path, "configs/c.1/acm.usb0"))
|
||||
_ = os.Remove(filepath.Join(gadget.path, "functions/acm.usb0"))
|
||||
_ = os.Remove(filepath.Join(gadget.path, "configs/c.1/strings/0x409"))
|
||||
_ = os.Remove(filepath.Join(gadget.path, "configs/c.1"))
|
||||
_ = os.Remove(filepath.Join(gadget.path, "strings/0x409"))
|
||||
_ = os.Remove(gadget.path)
|
||||
})
|
||||
|
||||
return gadget
|
||||
}
|
||||
|
||||
func TestBuildTargetsDedupesFixedBusID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -306,6 +409,40 @@ func TestLinuxHelpers(t *testing.T) {
|
||||
require.False(t, isUSBUEvent([]byte("ACTION=add\x00SUBSYSTEM=net\x00")))
|
||||
}
|
||||
|
||||
func TestServerStartRequiresHostDriver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedErr := errors.New("host driver unavailable")
|
||||
server := &ServerService{
|
||||
ctx: context.Background(),
|
||||
logger: newTestLogger(),
|
||||
exports: make(map[string]serverExport),
|
||||
controlSubs: make(map[uint64]*serverControlConn),
|
||||
ops: usbipOps{
|
||||
ensureHostDriver: func() error { return expectedErr },
|
||||
},
|
||||
}
|
||||
|
||||
err := server.Start(adapter.StartStateStart)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
}
|
||||
|
||||
func TestClientStartRequiresVHCI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedErr := errors.New("vhci unavailable")
|
||||
client := &ClientService{
|
||||
ctx: context.Background(),
|
||||
logger: newTestLogger(),
|
||||
ops: usbipOps{
|
||||
ensureVHCI: func() error { return expectedErr },
|
||||
},
|
||||
}
|
||||
|
||||
err := client.Start(adapter.StartStateStart)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
}
|
||||
|
||||
func TestServerReconcileExportsBindsMatchesAndSkipsHub(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -619,23 +756,42 @@ func TestClientRunControlSessionSyncsAssignmentsOnChanged(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUSBIPLinuxSmoke(t *testing.T) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("usbip smoke test requires root")
|
||||
}
|
||||
requireRoot(t)
|
||||
|
||||
require.NoError(t, ensureHostDriver())
|
||||
require.NoError(t, ensureVHCI())
|
||||
|
||||
busid := os.Getenv("USBIP_TEST_BUSID")
|
||||
if busid == "" {
|
||||
t.Skip("USBIP_TEST_BUSID not set")
|
||||
gadget := newTestUSBGadget(t)
|
||||
device, err := readSysfsDevice(gadget.busid, sysBusDevicePath(gadget.busid))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gadget.busid, device.BusID)
|
||||
|
||||
server := &ServerService{
|
||||
ctx: context.Background(),
|
||||
logger: newTestLogger(),
|
||||
exports: make(map[string]serverExport),
|
||||
controlSubs: make(map[uint64]*serverControlConn),
|
||||
ops: systemUSBIPOps,
|
||||
}
|
||||
require.NoError(t, server.bindOne(&device))
|
||||
|
||||
device, err := readSysfsDevice(busid, sysBusDevicePath(busid))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, busid, device.BusID)
|
||||
_, ok := server.snapshotExports()[gadget.busid]
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = currentDriver(busid)
|
||||
driver, err := currentDriver(gadget.busid)
|
||||
require.NoError(t, err)
|
||||
_, err = readUsbipStatus(busid)
|
||||
require.Equal(t, "usbip-host", driver)
|
||||
|
||||
status, err := readUsbipStatus(gadget.busid)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, usbipStatusAvailable, status)
|
||||
|
||||
require.NoError(t, hostUnbind(gadget.busid))
|
||||
require.NoError(t, hostMatchBusID(gadget.busid, false))
|
||||
require.NoError(t, bindToDriver(gadget.busid, "usb"))
|
||||
server.deleteExport(gadget.busid)
|
||||
|
||||
driver, err = currentDriver(gadget.busid)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "usb", driver)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user