Fix USB/IP import lease and attach races
This commit is contained in:
@@ -4,6 +4,7 @@ package usbip
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +18,8 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientService struct {
|
type ClientService struct {
|
||||||
@@ -215,25 +218,33 @@ func (c *ClientService) attemptAttach(ctx context.Context, busid string) (int, <
|
|||||||
c.logger.Debug("usbip client handoff ", busid, ": ", handoff.mode())
|
c.logger.Debug("usbip client handoff ", busid, ": ", handoff.mode())
|
||||||
c.portAssignAccess.Lock()
|
c.portAssignAccess.Lock()
|
||||||
defer c.portAssignAccess.Unlock()
|
defer c.portAssignAccess.Unlock()
|
||||||
port, err := c.ops.vhciPickFreePort(info.Speed)
|
triedPorts := make(map[int]struct{})
|
||||||
if err != nil {
|
for {
|
||||||
return -1, nil, err
|
port, err := c.ops.vhciPickFreePort(info.Speed, triedPorts)
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
if !c.reservePort(port) {
|
||||||
|
triedPorts[port] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = c.ops.vhciAttach(port, handoff.kernelFD(), info.DevID(), info.Speed)
|
||||||
|
if err != nil {
|
||||||
|
c.trackPort(port, false)
|
||||||
|
if errors.Is(err, unix.EBUSY) {
|
||||||
|
triedPorts[port] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return -1, nil, E.Cause(err, "vhci attach")
|
||||||
|
}
|
||||||
|
err = handoff.closeKernelFD()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Debug("close kernel fd ", busid, ": ", err)
|
||||||
|
}
|
||||||
|
done := handoff.startRelay(ctx, c.logger, "client", busid)
|
||||||
|
relayStarted = true
|
||||||
|
return port, done, nil
|
||||||
}
|
}
|
||||||
if !c.reservePort(port) {
|
|
||||||
return -1, nil, E.New("vhci port ", port, " already reserved")
|
|
||||||
}
|
|
||||||
err = c.ops.vhciAttach(port, handoff.kernelFD(), info.DevID(), info.Speed)
|
|
||||||
if err != nil {
|
|
||||||
c.trackPort(port, false)
|
|
||||||
return -1, nil, E.Cause(err, "vhci attach")
|
|
||||||
}
|
|
||||||
err = handoff.closeKernelFD()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Debug("close kernel fd ", busid, ": ", err)
|
|
||||||
}
|
|
||||||
done := handoff.startRelay(ctx, c.logger, "client", busid)
|
|
||||||
relayStarted = true
|
|
||||||
return port, done, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientService) waitPortSession(ctx context.Context, port int, busid string, done <-chan struct{}) {
|
func (c *ClientService) waitPortSession(ctx context.Context, port int, busid string, done <-chan struct{}) {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (s *ServerService) consumeImportLease(request ImportExtRequest) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
delete(s.leasesByBusID, request.BusID)
|
delete(s.leasesByBusID, request.BusID)
|
||||||
return now.Before(lease.Expires)
|
return now.Before(lease.Expires) && lease.Generation == s.controlSeq
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) cleanupExpiredImportLeasesLocked(now time.Time) {
|
func (s *ServerService) cleanupExpiredImportLeasesLocked(now time.Time) {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//go:build linux || (darwin && cgo)
|
||||||
|
|
||||||
|
package usbip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConsumeImportLeaseAcceptsCurrentGeneration(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const busid = "1-1"
|
||||||
|
server := &ServerService{
|
||||||
|
controlSeq: 7,
|
||||||
|
leasesByBusID: map[string]serverImportLease{
|
||||||
|
busid: {
|
||||||
|
ID: 55,
|
||||||
|
BusID: busid,
|
||||||
|
ClientNonce: 99,
|
||||||
|
Generation: 7,
|
||||||
|
Expires: time.Now().Add(importLeaseTTL),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := server.consumeImportLease(ImportExtRequest{
|
||||||
|
BusID: busid,
|
||||||
|
LeaseID: 55,
|
||||||
|
ClientNonce: 99,
|
||||||
|
})
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Empty(t, server.leasesByBusID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeImportLeaseRejectsStaleGeneration(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const busid = "1-1"
|
||||||
|
server := &ServerService{
|
||||||
|
controlSeq: 8,
|
||||||
|
leasesByBusID: map[string]serverImportLease{
|
||||||
|
busid: {
|
||||||
|
ID: 55,
|
||||||
|
BusID: busid,
|
||||||
|
ClientNonce: 99,
|
||||||
|
Generation: 7,
|
||||||
|
Expires: time.Now().Add(importLeaseTTL),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := server.consumeImportLease(ImportExtRequest{
|
||||||
|
BusID: busid,
|
||||||
|
LeaseID: 55,
|
||||||
|
ClientNonce: 99,
|
||||||
|
})
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Empty(t, server.leasesByBusID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeImportLeaseRejectsExpiredLease(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const busid = "1-1"
|
||||||
|
server := &ServerService{
|
||||||
|
controlSeq: 7,
|
||||||
|
leasesByBusID: map[string]serverImportLease{
|
||||||
|
busid: {
|
||||||
|
ID: 55,
|
||||||
|
BusID: busid,
|
||||||
|
ClientNonce: 99,
|
||||||
|
Generation: 7,
|
||||||
|
Expires: time.Now().Add(-time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := server.consumeImportLease(ImportExtRequest{
|
||||||
|
BusID: busid,
|
||||||
|
LeaseID: 55,
|
||||||
|
ClientNonce: 99,
|
||||||
|
})
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Empty(t, server.leasesByBusID)
|
||||||
|
}
|
||||||
+127
-6
@@ -278,7 +278,7 @@ func newTestUSBIPOps(t *testing.T) usbipOps {
|
|||||||
t.Fatalf("unexpected newUEventListener")
|
t.Fatalf("unexpected newUEventListener")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
vhciPickFreePort: func(uint32) (int, error) {
|
vhciPickFreePort: func(uint32, map[int]struct{}) (int, error) {
|
||||||
t.Fatalf("unexpected vhciPickFreePort")
|
t.Fatalf("unexpected vhciPickFreePort")
|
||||||
return 0, nil
|
return 0, nil
|
||||||
},
|
},
|
||||||
@@ -1946,7 +1946,8 @@ func TestClientAttemptAttachUsesImportReplyAndVHCIAttach(t *testing.T) {
|
|||||||
var attachedPort int
|
var attachedPort int
|
||||||
var attachedDevID uint32
|
var attachedDevID uint32
|
||||||
var attachedSpeed uint32
|
var attachedSpeed uint32
|
||||||
clientOps.vhciPickFreePort = func(speed uint32) (int, error) {
|
clientOps.vhciPickFreePort = func(speed uint32, skip map[int]struct{}) (int, error) {
|
||||||
|
require.Empty(t, skip)
|
||||||
require.Equal(t, SpeedSuper, speed)
|
require.Equal(t, SpeedSuper, speed)
|
||||||
return 7, nil
|
return 7, nil
|
||||||
}
|
}
|
||||||
@@ -2075,7 +2076,8 @@ func TestClientAttemptAttachUsesImportExtLease(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
ops := newTestUSBIPOps(t)
|
ops := newTestUSBIPOps(t)
|
||||||
ops.vhciPickFreePort = func(speed uint32) (int, error) {
|
ops.vhciPickFreePort = func(speed uint32, skip map[int]struct{}) (int, error) {
|
||||||
|
require.Empty(t, skip)
|
||||||
require.Equal(t, SpeedHigh, speed)
|
require.Equal(t, SpeedHigh, speed)
|
||||||
return 4, nil
|
return 4, nil
|
||||||
}
|
}
|
||||||
@@ -2164,7 +2166,8 @@ func TestClientAttemptAttachWithOpaqueConnRelay(t *testing.T) {
|
|||||||
|
|
||||||
kernelConnCh := make(chan net.Conn, 1)
|
kernelConnCh := make(chan net.Conn, 1)
|
||||||
ops := newTestUSBIPOps(t)
|
ops := newTestUSBIPOps(t)
|
||||||
ops.vhciPickFreePort = func(speed uint32) (int, error) {
|
ops.vhciPickFreePort = func(speed uint32, skip map[int]struct{}) (int, error) {
|
||||||
|
require.Empty(t, skip)
|
||||||
require.Equal(t, SpeedHigh, speed)
|
require.Equal(t, SpeedHigh, speed)
|
||||||
return 4, nil
|
return 4, nil
|
||||||
}
|
}
|
||||||
@@ -2226,6 +2229,123 @@ func TestClientAttemptAttachWithOpaqueConnRelay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientAttemptAttachRetriesNextPortOnEBUSY(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)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
device := newTestDevice("1-1", 0x1d6b, 0x0002, "serial-1", SpeedHigh)
|
||||||
|
serverErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
conn, acceptErr := listener.Accept()
|
||||||
|
if acceptErr != nil {
|
||||||
|
serverErrCh <- acceptErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
header, readErr := ReadOpHeader(conn)
|
||||||
|
if readErr != nil {
|
||||||
|
serverErrCh <- readErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if header.Code != OpReqImport {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected request code 0x%04x", header.Code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
busid, readErr := ReadOpReqImportBody(conn)
|
||||||
|
if readErr != nil {
|
||||||
|
serverErrCh <- readErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if busid != "1-1" {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected busid %s", busid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := device.toProtocol()
|
||||||
|
if writeErr := WriteOpRepImport(conn, OpStatusOK, &info); writeErr != nil {
|
||||||
|
serverErrCh <- writeErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer := make([]byte, 1)
|
||||||
|
n, readErr := conn.Read(buffer)
|
||||||
|
if n != 0 {
|
||||||
|
serverErrCh <- fmt.Errorf("unexpected server read bytes after relay close: %d", n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !errors.Is(readErr, io.EOF) {
|
||||||
|
serverErrCh <- readErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverErrCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
ops := newTestUSBIPOps(t)
|
||||||
|
var pickCalls int
|
||||||
|
var attachedPorts []int
|
||||||
|
ops.vhciPickFreePort = func(speed uint32, skip map[int]struct{}) (int, error) {
|
||||||
|
require.Equal(t, SpeedHigh, speed)
|
||||||
|
pickCalls++
|
||||||
|
if _, skipped := skip[4]; skipped {
|
||||||
|
return 5, nil
|
||||||
|
}
|
||||||
|
return 4, nil
|
||||||
|
}
|
||||||
|
ops.vhciAttach = func(port int, fd uintptr, devid uint32, speed uint32) error {
|
||||||
|
requireStreamSocketFD(t, fd)
|
||||||
|
info := device.toProtocol()
|
||||||
|
require.Equal(t, info.DevID(), devid)
|
||||||
|
require.Equal(t, SpeedHigh, speed)
|
||||||
|
attachedPorts = append(attachedPorts, port)
|
||||||
|
if port == 4 {
|
||||||
|
return unix.EBUSY
|
||||||
|
}
|
||||||
|
if port == 5 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unexpected vhci port %d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &ClientService{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
logger: newTestLogger(t),
|
||||||
|
dialer: wrappingDialer{},
|
||||||
|
serverAddr: M.SocksaddrFromNet(listener.Addr()),
|
||||||
|
ops: ops,
|
||||||
|
}
|
||||||
|
|
||||||
|
port, done, err := client.attemptAttach(ctx, "1-1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, done)
|
||||||
|
require.Equal(t, 5, port)
|
||||||
|
require.Equal(t, []int{4, 5}, attachedPorts)
|
||||||
|
require.Equal(t, 2, pickCalls)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("timed out waiting for client relay handoff")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-serverErrCh:
|
||||||
|
require.NoError(t, err)
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for server side close")
|
||||||
|
}
|
||||||
|
|
||||||
|
client.portsAccess.Lock()
|
||||||
|
_, firstReserved := client.ports[4]
|
||||||
|
_, secondReserved := client.ports[5]
|
||||||
|
client.portsAccess.Unlock()
|
||||||
|
require.False(t, firstReserved)
|
||||||
|
require.True(t, secondReserved)
|
||||||
|
}
|
||||||
|
|
||||||
func TestClientAttemptAttachRelayClosesHandoffOnVHCIAttachFailure(t *testing.T) {
|
func TestClientAttemptAttachRelayClosesHandoffOnVHCIAttachFailure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -2284,7 +2404,8 @@ func TestClientAttemptAttachRelayClosesHandoffOnVHCIAttachFailure(t *testing.T)
|
|||||||
expectedErr := errors.New("vhci attach failed")
|
expectedErr := errors.New("vhci attach failed")
|
||||||
kernelConnCh := make(chan net.Conn, 1)
|
kernelConnCh := make(chan net.Conn, 1)
|
||||||
ops := newTestUSBIPOps(t)
|
ops := newTestUSBIPOps(t)
|
||||||
ops.vhciPickFreePort = func(speed uint32) (int, error) {
|
ops.vhciPickFreePort = func(speed uint32, skip map[int]struct{}) (int, error) {
|
||||||
|
require.Empty(t, skip)
|
||||||
require.Equal(t, SpeedHigh, speed)
|
require.Equal(t, SpeedHigh, speed)
|
||||||
return 4, nil
|
return 4, nil
|
||||||
}
|
}
|
||||||
@@ -2574,7 +2695,7 @@ func TestClientAttemptAttachRejectsUnexpectedReplyVersion(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
ops := newTestUSBIPOps(t)
|
ops := newTestUSBIPOps(t)
|
||||||
ops.vhciPickFreePort = func(uint32) (int, error) {
|
ops.vhciPickFreePort = func(uint32, map[int]struct{}) (int, error) {
|
||||||
return -1, errors.New("unexpected vhci attach path")
|
return -1, errors.New("unexpected vhci attach path")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type usbipOps struct {
|
|||||||
writeUsbipSockfd func(busid string, fd int) error
|
writeUsbipSockfd func(busid string, fd int) error
|
||||||
newUEventListener func() (usbEventListener, error)
|
newUEventListener func() (usbEventListener, error)
|
||||||
|
|
||||||
vhciPickFreePort func(speed uint32) (int, error)
|
vhciPickFreePort func(speed uint32, skip map[int]struct{}) (int, error)
|
||||||
vhciAttach func(port int, fd uintptr, devid uint32, speed uint32) error
|
vhciAttach func(port int, fd uintptr, devid uint32, speed uint32) error
|
||||||
vhciDetach func(port int) error
|
vhciDetach func(port int) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type serverExport struct {
|
|||||||
device *darwinUSBHostDevice
|
device *darwinUSBHostDevice
|
||||||
entry DeviceEntry
|
entry DeviceEntry
|
||||||
busy bool
|
busy bool
|
||||||
|
stale bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type darwinUSBHostDeviceWatch interface {
|
type darwinUSBHostDeviceWatch interface {
|
||||||
@@ -203,6 +204,7 @@ func (s *ServerService) reconcileExports() (bool, error) {
|
|||||||
}
|
}
|
||||||
if export, ok := current[busid]; ok {
|
if export, ok := current[busid]; ok {
|
||||||
if export.busy {
|
if export.busy {
|
||||||
|
changed = s.markExportStale(busid) || changed
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.deleteExport(busid)
|
s.deleteExport(busid)
|
||||||
@@ -229,6 +231,7 @@ func (s *ServerService) reconcileExports() (bool, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if export.busy {
|
if export.busy {
|
||||||
|
changed = s.markExportStale(busid) || changed
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.deleteExport(busid)
|
s.deleteExport(busid)
|
||||||
@@ -270,7 +273,7 @@ func (s *ServerService) currentExports() []serverExport {
|
|||||||
defer s.access.Unlock()
|
defer s.access.Unlock()
|
||||||
out := make([]serverExport, 0, len(s.exports))
|
out := make([]serverExport, 0, len(s.exports))
|
||||||
for _, export := range s.exports {
|
for _, export := range s.exports {
|
||||||
if export.busy {
|
if export.busy || export.stale {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, export)
|
out = append(out, export)
|
||||||
@@ -320,7 +323,7 @@ func (s *ServerService) claimExport(busid string) (serverExport, bool) {
|
|||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
defer s.access.Unlock()
|
defer s.access.Unlock()
|
||||||
export, ok := s.exports[busid]
|
export, ok := s.exports[busid]
|
||||||
if !ok || export.busy {
|
if !ok || export.busy || export.stale {
|
||||||
return serverExport{}, false
|
return serverExport{}, false
|
||||||
}
|
}
|
||||||
export.busy = true
|
export.busy = true
|
||||||
@@ -328,14 +331,35 @@ func (s *ServerService) claimExport(busid string) (serverExport, bool) {
|
|||||||
return export, true
|
return export, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) releaseClaim(busid string) {
|
func (s *ServerService) markExportStale(busid string) bool {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
|
defer s.access.Unlock()
|
||||||
export, ok := s.exports[busid]
|
export, ok := s.exports[busid]
|
||||||
if ok {
|
if !ok || export.stale {
|
||||||
export.busy = false
|
return false
|
||||||
s.exports[busid] = export
|
|
||||||
}
|
}
|
||||||
|
export.stale = true
|
||||||
|
s.exports[busid] = export
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) releaseClaim(claimed serverExport) bool {
|
||||||
|
s.access.Lock()
|
||||||
|
export, ok := s.exports[claimed.busid]
|
||||||
|
if !ok || export.registryID != claimed.registryID {
|
||||||
|
s.access.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if export.stale {
|
||||||
|
delete(s.exports, claimed.busid)
|
||||||
|
s.access.Unlock()
|
||||||
|
export.device.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
export.busy = false
|
||||||
|
s.exports[claimed.busid] = export
|
||||||
s.access.Unlock()
|
s.access.Unlock()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
|
func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
|
||||||
@@ -440,7 +464,12 @@ func (s *ServerService) handleImportBusID(conn net.Conn, busid string, extended
|
|||||||
releaseClaim := true
|
releaseClaim := true
|
||||||
defer func() {
|
defer func() {
|
||||||
if releaseClaim {
|
if releaseClaim {
|
||||||
s.releaseClaim(busid)
|
if s.releaseClaim(export) {
|
||||||
|
if err := s.reconcileAndBroadcast(true); err != nil {
|
||||||
|
s.logger.Warn("reconcile exports after stale release: ", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
s.broadcastChanged()
|
s.broadcastChanged()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -456,8 +485,14 @@ func (s *ServerService) handleImportBusID(conn net.Conn, busid string, extended
|
|||||||
if err != nil && s.ctx.Err() == nil {
|
if err != nil && s.ctx.Err() == nil {
|
||||||
s.logger.Debug("data session ", busid, ": ", err)
|
s.logger.Debug("data session ", busid, ": ", err)
|
||||||
}
|
}
|
||||||
s.releaseClaim(busid)
|
stale := s.releaseClaim(export)
|
||||||
releaseClaim = false
|
releaseClaim = false
|
||||||
|
if stale {
|
||||||
|
if err := s.reconcileAndBroadcast(true); err != nil {
|
||||||
|
s.logger.Warn("reconcile exports after stale release: ", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
s.broadcastChanged()
|
s.broadcastChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +507,9 @@ func (s *ServerService) buildDeviceStateV2() []DeviceInfoV2 {
|
|||||||
}
|
}
|
||||||
devices := make([]DeviceInfoV2, 0, len(exports))
|
devices := make([]DeviceInfoV2, 0, len(exports))
|
||||||
for _, export := range exports {
|
for _, export := range exports {
|
||||||
|
if export.stale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
state := deviceStateAvailable
|
state := deviceStateAvailable
|
||||||
reason := deviceStateAvailable
|
reason := deviceStateAvailable
|
||||||
if export.busy {
|
if export.busy {
|
||||||
@@ -490,6 +528,9 @@ func (s *ServerService) leaseAvailable(busid string) (bool, string) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return false, "unknown busid"
|
return false, "unknown busid"
|
||||||
}
|
}
|
||||||
|
if export.stale {
|
||||||
|
return false, deviceStateUnavailable
|
||||||
|
}
|
||||||
if export.busy {
|
if export.busy {
|
||||||
return false, deviceStateBusy
|
return false, deviceStateBusy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,98 @@ func TestDarwinServerBuildDeviceStateIncludesBusyExports(t *testing.T) {
|
|||||||
require.Equal(t, deviceStateBusy, devices["busy"].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) {
|
func TestDarwinServerRegisterControlConnQueuesSnapshotBeforeBroadcast(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -310,6 +402,20 @@ func darwinServerControlState(server *ServerService, busid string) string {
|
|||||||
return server.controlState[busid].State
|
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 {
|
type fakeDarwinUSBHostDeviceWatch struct {
|
||||||
callback func()
|
callback func()
|
||||||
closed bool
|
closed bool
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func writeUsbipSockfd(busid string, fd int) error {
|
|||||||
return writeSysfs(filepath.Join(sysBusUSBDevices, busid, "usbip_sockfd"), strconv.Itoa(fd))
|
return writeSysfs(filepath.Join(sysBusUSBDevices, busid, "usbip_sockfd"), strconv.Itoa(fd))
|
||||||
}
|
}
|
||||||
|
|
||||||
func vhciPickFreePort(speed uint32) (int, error) {
|
func vhciPickFreePort(speed uint32, skip map[int]struct{}) (int, error) {
|
||||||
records, err := readVHCIStatus()
|
records, err := readVHCIStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
@@ -241,6 +241,9 @@ func vhciPickFreePort(speed uint32) (int, error) {
|
|||||||
if record.hub != targetHub || record.state != 4 {
|
if record.hub != targetHub || record.state != 4 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, skipped := skip[record.port]; skipped {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return record.port, nil
|
return record.port, nil
|
||||||
}
|
}
|
||||||
return -1, E.New("no free ", targetHub, " vhci port")
|
return -1, E.New("no free ", targetHub, " vhci port")
|
||||||
|
|||||||
Reference in New Issue
Block a user