diff --git a/common/vboxusb/device_windows.go b/common/vboxusb/device_windows.go index 5aa9746d4..ac90d16f9 100644 --- a/common/vboxusb/device_windows.go +++ b/common/vboxusb/device_windows.go @@ -177,13 +177,6 @@ type IsoPacket struct { Status URBError } -// URBResult mirrors the fields VBoxUSB writes back into the URB struct. -type URBResult struct { - Error URBError - Length uint64 - IsoPackets []IsoPacket -} - // urbStructSize is the on-the-wire size of USBSUP_URB with Pack=4 on // 64-bit systems (both amd64 and arm64; nint is 8 bytes either way). // Layout (offsets in bytes): @@ -206,9 +199,6 @@ const urbStructSize = 104 // Caller must keep urb.Buffer alive across the call (SendURB does so // internally for the duration of the syscall). func (d *Device) SendURB(urb *URB) error { - if len(urb.IsoPackets) > MaxIsoPacketsPerURB { - return E.New("vboxusb: too many iso packets: ", len(urb.IsoPackets), " > ", MaxIsoPacketsPerURB) - } var raw [urbStructSize]byte binary.LittleEndian.PutUint32(raw[0:4], uint32(urb.Type)) binary.LittleEndian.PutUint32(raw[4:8], urb.Endpoint) @@ -277,14 +267,14 @@ func (e *URBStatusError) Error() string { } } -// ioctl is the single synchronous overlapped DeviceIoControl primitive. -// Ported from common/windivert/handle_windows.go:263-290. The event is -// the per-Device event (reused across calls) so we avoid CreateEvent on -// every URB. func (d *Device) ioctl(code uint32, in []byte, out []byte) (uint32, error) { + return overlappedIoctl(d.handle, code, in, out, d.event) +} + +func overlappedIoctl(handle windows.Handle, code uint32, in []byte, out []byte, event windows.Handle) (uint32, error) { var overlapped windows.Overlapped - overlapped.HEvent = d.event - _ = windows.ResetEvent(d.event) + overlapped.HEvent = event + _ = windows.ResetEvent(event) var inPtr *byte var inLen uint32 if len(in) > 0 { @@ -298,11 +288,11 @@ func (d *Device) ioctl(code uint32, in []byte, out []byte) (uint32, error) { outLen = uint32(len(out)) } var returned uint32 - err := windows.DeviceIoControl(d.handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped) + err := windows.DeviceIoControl(handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped) if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) { return 0, err } - err = windows.GetOverlappedResult(d.handle, &overlapped, &returned, true) + err = windows.GetOverlappedResult(handle, &overlapped, &returned, true) if err != nil { return 0, err } diff --git a/common/vboxusb/driver_windows.go b/common/vboxusb/driver_windows.go index 4853e7d38..9a40f732d 100644 --- a/common/vboxusb/driver_windows.go +++ b/common/vboxusb/driver_windows.go @@ -42,10 +42,6 @@ var ( ) func installDrivers() error { - if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { - return E.New("vboxusb: unsupported GOARCH ", runtime.GOARCH) - } - dir, err := ensureExtracted() if err != nil { return err diff --git a/common/vboxusb/monitor_windows.go b/common/vboxusb/monitor_windows.go index 63ef3edc7..803e5e3d8 100644 --- a/common/vboxusb/monitor_windows.go +++ b/common/vboxusb/monitor_windows.go @@ -126,31 +126,7 @@ func (m *Monitor) RemoveFilter(id uint64) error { } func (m *Monitor) ioctl(code uint32, in []byte, out []byte) (uint32, error) { - var overlapped windows.Overlapped - overlapped.HEvent = m.event - _ = windows.ResetEvent(m.event) - var inPtr *byte - var inLen uint32 - if len(in) > 0 { - inPtr = &in[0] - inLen = uint32(len(in)) - } - var outPtr *byte - var outLen uint32 - if len(out) > 0 { - outPtr = &out[0] - outLen = uint32(len(out)) - } - var returned uint32 - err := windows.DeviceIoControl(m.handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped) - if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) { - return 0, err - } - err = windows.GetOverlappedResult(m.handle, &overlapped, &returned, true) - if err != nil { - return 0, err - } - return returned, nil + return overlappedIoctl(m.handle, code, in, out, m.event) } // encodeFilter builds a 312-byte USBFILTER packed struct matching the diff --git a/common/vboxusb/pnp_windows.go b/common/vboxusb/pnp_windows.go index b4ed12e34..0f44bffe7 100644 --- a/common/vboxusb/pnp_windows.go +++ b/common/vboxusb/pnp_windows.go @@ -3,19 +3,30 @@ package vboxusb import ( - "encoding/binary" "errors" "strconv" "strings" - "sync" "time" - "unsafe" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/sys/windows" ) +var MonitorAccessGUID = windows.GUID{ + Data1: 0x00873fdf, + Data2: 0xCAFE, + Data3: 0x80EE, + Data4: [8]byte{0xaa, 0x5e, 0x00, 0xc0, 0x4f, 0xb1, 0x72, 0x0b}, +} + +var USBDeviceInterfaceGUID = windows.GUID{ + Data1: 0xa5dcbf10, + Data2: 0x6530, + Data3: 0x11d2, + Data4: [8]byte{0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed}, +} + // USBDeviceInfo describes one USB device enumerated by Windows // regardless of which function driver currently owns it. Bus/Address // values are normalized into a stable bus-id string ("-
") @@ -36,13 +47,9 @@ type USBDeviceInfo struct { // one record per attached USB device. Hubs are not filtered out here; // the caller (export host) skips DeviceClass == 0x09. func EnumerateUSBDevices() ([]USBDeviceInfo, error) { + guid := USBDeviceInterfaceGUID devInfo, err := windows.SetupDiGetClassDevsEx( - &windows.GUID{ - Data1: USBDeviceInterfaceGUID.Data1, - Data2: USBDeviceInterfaceGUID.Data2, - Data3: USBDeviceInterfaceGUID.Data3, - Data4: USBDeviceInterfaceGUID.Data4, - }, + &guid, "", 0, windows.DIGCF_PRESENT|windows.DIGCF_DEVICEINTERFACE, @@ -87,62 +94,8 @@ func EnumerateUSBDevices() ([]USBDeviceInfo, error) { return out, nil } -// CycleHubPort issues IOCTL_USB_HUB_CYCLE_PORT on the parent hub for -// the given 1-based port number. This is the supported equivalent of -// physically unplugging and replugging the device; combined with a -// VBoxUSBMon filter it triggers PnP to bind VBoxUSB to the device. -// -// hubInterfacePath is the setupapi-resolved interface path of the -// parent hub (obtained via CM_Get_Device_Interface_List with -// USBHubInterfaceGUID). -func CycleHubPort(hubInterfacePath string, port uint32) error { - pathW, err := windows.UTF16PtrFromString(hubInterfacePath) - if err != nil { - return E.Cause(err, "vboxusb: utf16 hub path") - } - handle, err := windows.CreateFile( - pathW, - windows.GENERIC_READ|windows.GENERIC_WRITE, - windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, - nil, - windows.OPEN_EXISTING, - windows.FILE_ATTRIBUTE_NORMAL, - 0, - ) - if err != nil { - return E.Cause(err, "vboxusb: open hub ", hubInterfacePath) - } - defer windows.CloseHandle(handle) - - // USB_CYCLE_PORT_PARAMS: ULONG ConnectionIndex; ULONG StatusReturned; - var params [8]byte - binary.LittleEndian.PutUint32(params[0:4], port) - var returned uint32 - err = windows.DeviceIoControl( - handle, - IOCTLHubCyclePort, - ¶ms[0], uint32(len(params)), - ¶ms[0], uint32(len(params)), - &returned, nil, - ) - if err != nil { - return E.Cause(err, "vboxusb: IOCTL_USB_HUB_CYCLE_PORT") - } - return nil -} - -// WaitForVBoxUSBInterface polls CM_Get_Device_Interface_List for the -// VBoxUSB class GUID until an interface path matching instanceID -// appears, or timeout elapses. After RestartDevice + filter trigger, -// PnP needs a moment to load VBoxUSB.sys on the device; matching -// usbipd-win's 10-second window. func WaitForVBoxUSBInterface(instanceID string, timeout time.Duration) (string, error) { - guid := windows.GUID{ - Data1: MonitorAccessGUID.Data1, - Data2: MonitorAccessGUID.Data2, - Data3: MonitorAccessGUID.Data3, - Data4: MonitorAccessGUID.Data4, - } + guid := MonitorAccessGUID deadline := time.Now().Add(timeout) for { paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) @@ -160,39 +113,6 @@ func WaitForVBoxUSBInterface(instanceID string, timeout time.Duration) (string, } } -// RestartDevice triggers the PnP unplug/replug cycle that lets a -// pending VBoxUSBMon CAPTURE filter activate. usbipd-win uses -// CM_Query_And_Remove_SubTree -> IOCTL_USB_HUB_CYCLE_PORT -> -// CM_Setup_DevNode. -// -// TODO(phase B follow-up): CM_Query_And_Remove_SubTree and -// CM_Setup_DevNode are not exposed by golang.org/x/sys/windows and -// need direct LazyDLL wrappers. Until that is in place, callers -// should arrange for the device to be physically replugged. -func RestartDevice(_ string) error { - return E.New("vboxusb: RestartDevice not yet implemented (use CycleHubPort for now)") -} - -// WatchDeviceArrival registers a callback for device arrival/removal -// on GUID_DEVINTERFACE_USB_DEVICE. Returns a handle that must be -// closed when the watcher is no longer needed. -// -// TODO(phase B follow-up): CM_Register_Notification is not exposed by -// golang.org/x/sys/windows. Until wired up, the export host will need -// to poll via EnumerateUSBDevices on a reconcile timer. -func WatchDeviceArrival(_ func()) (DeviceWatcher, error) { - return nil, E.New("vboxusb: WatchDeviceArrival not yet implemented (poll EnumerateUSBDevices instead)") -} - -// DeviceWatcher is the handle returned by WatchDeviceArrival. Close -// stops delivery and releases the underlying CM_Notify_HNOTIFICATION. -type DeviceWatcher interface { - Close() error -} - -// firstString returns the first NUL-separated string in a REG_MULTI_SZ -// value (returned by SetupDiGetDeviceRegistryProperty for HardwareID). -// Returns the value unchanged for REG_SZ. func firstString(value any) string { switch v := value.(type) { case string: @@ -215,9 +135,6 @@ func toUint32(value any) uint32 { return 0 } -// parseHardwareID extracts VID/PID/REV from a USB hardware ID such as -// "USB\VID_046D&PID_C31C&REV_6400". Returns zeros on parse failure; -// the caller decides whether that disqualifies the device. func parseHardwareID(hwid string) (vid, pid, rev uint16) { upper := strings.ToUpper(hwid) vid = extractHex16(upper, "VID_") @@ -252,14 +169,3 @@ func extractHex16(s, prefix string) uint16 { func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r >= 'A' && r <= 'F') || (r >= 'a' && r <= 'f') } - -// pnpProcOnce + LazyProc handles for direct syscall to functions that -// golang.org/x/sys/windows does not yet wrap. Kept in one place so the -// RestartDevice / WatchDeviceArrival follow-up has the resolver -// boilerplate already laid out. -var ( - pnpProcOnce sync.Once - modCfgMgr32 = windows.NewLazyDLL("cfgmgr32.dll") - _ = modCfgMgr32 // referenced by upcoming RestartDevice impl - _ unsafe.Pointer -) diff --git a/common/vboxusb/vboxusb.go b/common/vboxusb/vboxusb.go index a373bd0eb..64caac670 100644 --- a/common/vboxusb/vboxusb.go +++ b/common/vboxusb/vboxusb.go @@ -43,46 +43,6 @@ const ( MonitorDevicePath = `\\.\VBoxUSBMon` ) -// MonitorAccessGUID is GUID_CLASS_VBOXUSB from VirtualBox usblib-win.h. -// Used with SetupDiEnumDeviceInterfaces to find the per-device file -// path after VBoxUSB binds. -var MonitorAccessGUID = GUID{ - Data1: 0x00873fdf, - Data2: 0xCAFE, - Data3: 0x80EE, - Data4: [8]byte{0xaa, 0x5e, 0x00, 0xc0, 0x4f, 0xb1, 0x72, 0x0b}, -} - -// USBDeviceInterfaceGUID is GUID_DEVINTERFACE_USB_DEVICE -// ({a5dcbf10-6530-11d2-901f-00c04fb951ed}). Used to enumerate plugged -// USB devices regardless of which function driver currently owns them. -var USBDeviceInterfaceGUID = GUID{ - Data1: 0xa5dcbf10, - Data2: 0x6530, - Data3: 0x11d2, - Data4: [8]byte{0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed}, -} - -// USBHubInterfaceGUID is GUID_DEVINTERFACE_USB_HUB -// ({f18a0e88-c30c-11d0-8815-00a0c906bed8}). The parent hub of a target -// device is opened with this GUID to issue IOCTL_USB_HUB_CYCLE_PORT. -var USBHubInterfaceGUID = GUID{ - Data1: 0xf18a0e88, - Data2: 0xc30c, - Data3: 0x11d0, - Data4: [8]byte{0x88, 0x15, 0x00, 0xa0, 0xc9, 0x06, 0xbe, 0xd8}, -} - -// GUID matches the Windows GUID layout exactly. We carry our own copy -// so the (cross-platform) package-level vars above can be declared -// without depending on golang.org/x/sys/windows. -type GUID struct { - Data1 uint32 - Data2 uint16 - Data3 uint16 - Data4 [8]byte -} - // IOCTL codes from VirtualBox usblib-win.h, identical to those used by // usbipd-win (Usbipd/Interop/VBoxUsb.cs:26-39 and VBoxUsbMon.cs:122-129). // Encoding is the standard CTL_CODE shape: @@ -108,11 +68,6 @@ const ( IOCTLMonitorRemoveFilter uint32 = 0x0022_1848 ) -// IOCTLHubCyclePort = IOCTL_USB_HUB_CYCLE_PORT from usbioctl.h -// (FILE_DEVICE_USB=0x22, FILE_ANY_ACCESS=0, function=0x111, -// METHOD_BUFFERED=0). -const IOCTLHubCyclePort uint32 = 0x0022_0444 - // USB/IP-style transfer type enum, matching VirtualBox USBSUP_TRANSFER_TYPE. type TransferType uint32 diff --git a/service/usbip/client.go b/service/usbip/client.go index f4f00262c..50a8ebdb6 100644 --- a/service/usbip/client.go +++ b/service/usbip/client.go @@ -32,8 +32,8 @@ type ClientService struct { assignedWorkers []*clientAssignedWorker allWorkers map[string]context.CancelFunc - remoteAccess sync.Mutex - remoteDevicesV2 map[string]DeviceInfoV2 + remoteAccess sync.Mutex + remoteDevices map[string]ControlDeviceInfo } func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) { diff --git a/service/usbip/client_assignment.go b/service/usbip/client_assignment.go index 42906ddcd..2ccd211b7 100644 --- a/service/usbip/client_assignment.go +++ b/service/usbip/client_assignment.go @@ -9,9 +9,6 @@ import ( "github.com/sagernet/sing-box/option" ) -// clientAssignment runs in two modes that share active-busid tracking: -// matched (len(targets) > 0) binds each target to at most one busid; -// import-all (len(targets) == 0) marks every advertised busid as desired. type clientAssignment struct { access sync.Mutex @@ -82,9 +79,6 @@ func (a *clientAssignment) ApplyMatched(entries []DeviceEntry, knownKeys map[str return nextAssigned, prev } -// ApplyAll keeps no-longer-desired-but-active busids registered so the -// runBusIDLoop exits naturally after the active session ends, via -// IsRetryDesired returning false. func (a *clientAssignment) ApplyAll(entries []DeviceEntry) (start []string, stop []string) { desired := make(map[string]struct{}, len(entries)) for i := range entries { diff --git a/service/usbip/client_darwin.go b/service/usbip/client_darwin.go index 0baf51ad0..581cc1e41 100644 --- a/service/usbip/client_darwin.go +++ b/service/usbip/client_darwin.go @@ -42,7 +42,6 @@ type darwinVirtualController struct { runErr error eventStarted atomic.Bool - stateAccess sync.Mutex powered bool connected bool nextAddress uint8 @@ -136,7 +135,6 @@ func (c *darwinVirtualController) enqueueEvent(event darwinControllerEvent) { } func (c *darwinVirtualController) eventLoop() { - c.eventStarted.Store(true) defer close(c.eventDone) defer c.teardownIOUSBHostState() for { @@ -200,27 +198,21 @@ func (c *darwinVirtualController) handleDeviceCreate(message darwinCIMessage) er if err != nil { return err } - c.stateAccess.Lock() address := c.nextAddress c.nextAddress++ c.devices[address] = device - c.stateAccess.Unlock() return device.respondCreate(message, ciStatusSuccess, address) } func (c *darwinVirtualController) handleDeviceCommand(message darwinCIMessage) error { address := message.deviceAddress() - c.stateAccess.Lock() device := c.devices[address] - c.stateAccess.Unlock() if device == nil { return nil } err := device.respond(message, ciStatusSuccess) if message.messageType() == ciMsgDeviceDestroy { - c.stateAccess.Lock() delete(c.devices, address) - c.stateAccess.Unlock() device.Close() } return err @@ -233,21 +225,17 @@ func (c *darwinVirtualController) handleEndpointCreate(message darwinCIMessage) } key := darwinEndpointKey{device: message.deviceAddress(), endpoint: message.endpointAddress()} endpoint := newDarwinEndpoint(c.ctx, c.logger, sm, c.peer, c.CurrentFrame, c.info.DevID(), key) - c.stateAccess.Lock() c.endpoints[key] = endpoint - c.stateAccess.Unlock() return sm.respond(message, ciStatusSuccess) } func (c *darwinVirtualController) handleEndpointCommand(message darwinCIMessage) error { key := darwinEndpointKey{device: message.deviceAddress(), endpoint: message.endpointAddress()} destroy := message.messageType() == ciMsgEndpointDestroy - c.stateAccess.Lock() endpoint := c.endpoints[key] if destroy { delete(c.endpoints, key) } - c.stateAccess.Unlock() if endpoint == nil { return nil } @@ -263,9 +251,7 @@ func (c *darwinVirtualController) handleDoorbell(doorbell uint32) { device: uint8(doorbell & 0xff), endpoint: uint8((doorbell >> 8) & 0xff), } - c.stateAccess.Lock() endpoint := c.endpoints[key] - c.stateAccess.Unlock() if endpoint == nil { return } @@ -273,7 +259,6 @@ func (c *darwinVirtualController) handleDoorbell(doorbell uint32) { } func (c *darwinVirtualController) teardownIOUSBHostState() { - c.stateAccess.Lock() endpoints := make([]*darwinEndpoint, 0, len(c.endpoints)) for _, endpoint := range c.endpoints { endpoints = append(endpoints, endpoint) @@ -286,7 +271,6 @@ func (c *darwinVirtualController) teardownIOUSBHostState() { c.devices = make(map[uint8]*darwinUSBHostDeviceSM) controller := c.controller c.controller = nil - c.stateAccess.Unlock() for _, endpoint := range endpoints { endpoint.Close() @@ -294,7 +278,5 @@ func (c *darwinVirtualController) teardownIOUSBHostState() { for _, device := range devices { device.Close() } - if controller != nil { - controller.Close() - } + controller.Close() } diff --git a/service/usbip/client_shared.go b/service/usbip/client_shared.go index 80604bbec..451ebbecf 100644 --- a/service/usbip/client_shared.go +++ b/service/usbip/client_shared.go @@ -115,10 +115,6 @@ func (c *ClientService) run() { } } -// Dynamic export discovery and hotplug updates are provided by the sing-box -// USB/IP control extensions. Standard USB/IP implementations expose only a -// static DEVLIST snapshot, so we seed assignments once and do not keep polling -// for newly exported devices. func (c *ClientService) runStandardStaticMode() error { err := c.syncRemoteStateContext(c.ctx) if err != nil { @@ -199,10 +195,10 @@ func (c *ClientService) runControlSession() error { if err != nil { return E.Cause(errImmediateReconnect, "read device snapshot: ", err) } - devices := deviceInfoV2Map(snapshot.Devices) - values := sortedDeviceInfoV2Values(devices) + devices := controlDeviceInfoMap(snapshot.Devices) + values := sortedControlDeviceInfoValues(devices) c.remoteAccess.Lock() - c.remoteDevicesV2 = devices + c.remoteDevices = devices c.remoteAccess.Unlock() c.applyRemoteDeviceState(values) case controlFramePong: @@ -249,8 +245,8 @@ func (c *ClientService) syncRemoteStateContext(ctx context.Context) error { return nil } -func (c *ClientService) applyRemoteDeviceState(devices []DeviceInfoV2) { - availableEntries := deviceInfoV2ToEntries(devices, true) +func (c *ClientService) applyRemoteDeviceState(devices []ControlDeviceInfo) { + availableEntries := controlDeviceInfoToEntries(devices, true) if !c.assignment.Matched() { c.applyRemoteExports(availableEntries) return diff --git a/service/usbip/control_protocol.go b/service/usbip/control_protocol.go index 4e798ba4a..365726f74 100644 --- a/service/usbip/control_protocol.go +++ b/service/usbip/control_protocol.go @@ -16,9 +16,9 @@ const ( controlFrameHello uint8 = 1 controlFrameAck uint8 = 2 - controlFramePing uint8 = 4 - controlFramePong uint8 = 5 - controlFrameDeviceSnapshot uint8 = 6 + controlFramePing uint8 = 3 + controlFramePong uint8 = 4 + controlFrameDeviceSnapshot uint8 = 5 controlPrefaceSize = 8 controlFrameSize = 4 @@ -46,40 +46,39 @@ type controlMessage struct { Payload []byte } -type DeviceInterfaceV2 struct { +type ControlDeviceInterface struct { Class uint8 `json:"class"` SubClass uint8 `json:"subclass"` Protocol uint8 `json:"protocol"` } -type DeviceInfoV2 struct { - BusID string `json:"busid"` - StableID string `json:"stable_id,omitempty"` - Backend string `json:"backend,omitempty"` - Path string `json:"path,omitempty"` - Serial string `json:"serial,omitempty"` - VendorID uint16 `json:"vendor_id"` - ProductID uint16 `json:"product_id"` - BCDDevice uint16 `json:"bcd_device,omitempty"` - Speed uint32 `json:"speed"` - DeviceClass uint8 `json:"device_class"` - DeviceSubClass uint8 `json:"device_subclass"` - DeviceProtocol uint8 `json:"device_protocol"` - ConfigurationValue uint8 `json:"configuration_value"` - NumConfigurations uint8 `json:"num_configurations"` - NumInterfaces uint8 `json:"num_interfaces"` - Interfaces []DeviceInterfaceV2 `json:"interfaces,omitempty"` - State string `json:"state"` - StatusCode int `json:"status_code,omitempty"` - StatusReason string `json:"status_reason,omitempty"` +type ControlDeviceInfo struct { + BusID string `json:"busid"` + StableID string `json:"stable_id,omitempty"` + Backend string `json:"backend,omitempty"` + Path string `json:"path,omitempty"` + Serial string `json:"serial,omitempty"` + VendorID uint16 `json:"vendor_id"` + ProductID uint16 `json:"product_id"` + BCDDevice uint16 `json:"bcd_device,omitempty"` + Speed uint32 `json:"speed"` + DeviceClass uint8 `json:"device_class"` + DeviceSubClass uint8 `json:"device_subclass"` + DeviceProtocol uint8 `json:"device_protocol"` + ConfigurationValue uint8 `json:"configuration_value"` + NumConfigurations uint8 `json:"num_configurations"` + NumInterfaces uint8 `json:"num_interfaces"` + Interfaces []ControlDeviceInterface `json:"interfaces,omitempty"` + State string `json:"state"` + StatusCode int `json:"status_code,omitempty"` + StatusReason string `json:"status_reason,omitempty"` } type controlDeviceSnapshot struct { - Devices []DeviceInfoV2 `json:"devices"` + Devices []ControlDeviceInfo `json:"devices"` } -// controlReader reuses its payload scratch across successive reads on a -// single connection. The returned payload is only valid until the next call. +// The returned payload is only valid until the next call. type controlReader struct { scratch []byte } @@ -151,10 +150,10 @@ func unmarshalControlPayload(payload []byte, value any) error { return json.Unmarshal(payload, value) } -func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, state string, statusCode int, statusReason string) DeviceInfoV2 { - interfaces := make([]DeviceInterfaceV2, len(entry.Interfaces)) +func controlDeviceInfoFromEntry(entry DeviceEntry, backend string, stableID string, state string, statusCode int, statusReason string) ControlDeviceInfo { + interfaces := make([]ControlDeviceInterface, len(entry.Interfaces)) for i := range entry.Interfaces { - interfaces[i] = DeviceInterfaceV2{ + interfaces[i] = ControlDeviceInterface{ Class: entry.Interfaces[i].BInterfaceClass, SubClass: entry.Interfaces[i].BInterfaceSubClass, Protocol: entry.Interfaces[i].BInterfaceProtocol, @@ -167,7 +166,7 @@ func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, s if serial == "" { serial = entry.Info.SerialString() } - return DeviceInfoV2{ + return ControlDeviceInfo{ BusID: entry.Info.BusIDString(), StableID: stableID, Backend: backend, @@ -190,8 +189,8 @@ func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, s } } -func deviceInfoV2Map(devices []DeviceInfoV2) map[string]DeviceInfoV2 { - out := make(map[string]DeviceInfoV2, len(devices)) +func controlDeviceInfoMap(devices []ControlDeviceInfo) map[string]ControlDeviceInfo { + out := make(map[string]ControlDeviceInfo, len(devices)) for _, device := range devices { if device.BusID == "" { continue @@ -201,20 +200,20 @@ func deviceInfoV2Map(devices []DeviceInfoV2) map[string]DeviceInfoV2 { return out } -func sortedDeviceInfoV2Values(devices map[string]DeviceInfoV2) []DeviceInfoV2 { +func sortedControlDeviceInfoValues(devices map[string]ControlDeviceInfo) []ControlDeviceInfo { busids := make([]string, 0, len(devices)) for busid := range devices { busids = append(busids, busid) } slices.Sort(busids) - out := make([]DeviceInfoV2, 0, len(busids)) + out := make([]ControlDeviceInfo, 0, len(busids)) for _, busid := range busids { out = append(out, devices[busid]) } return out } -func deviceInfoV2ToEntries(devices []DeviceInfoV2, availableOnly bool) []DeviceEntry { +func controlDeviceInfoToEntries(devices []ControlDeviceInfo, availableOnly bool) []DeviceEntry { entries := make([]DeviceEntry, 0, len(devices)) for _, device := range devices { if availableOnly && device.State != "" && device.State != deviceStateAvailable { @@ -246,7 +245,7 @@ func deviceInfoV2ToEntries(devices []DeviceInfoV2, availableOnly bool) []DeviceE return entries } -func deviceInfoV2Equal(a, b DeviceInfoV2) bool { +func controlDeviceInfoEqual(a, b ControlDeviceInfo) bool { if a.BusID != b.BusID || a.StableID != b.StableID || a.Backend != b.Backend || @@ -267,13 +266,5 @@ func deviceInfoV2Equal(a, b DeviceInfoV2) bool { a.StatusReason != b.StatusReason { return false } - if len(a.Interfaces) != len(b.Interfaces) { - return false - } - for i := range a.Interfaces { - if a.Interfaces[i] != b.Interfaces[i] { - return false - } - } - return true + return slices.Equal(a.Interfaces, b.Interfaces) } diff --git a/service/usbip/darwin_integration_test.go b/service/usbip/darwin_integration_test.go index 9f038a00b..8b0a51134 100644 --- a/service/usbip/darwin_integration_test.go +++ b/service/usbip/darwin_integration_test.go @@ -279,7 +279,7 @@ func (s *darwinFakeUSBIPServer) handleControlConn(conn net.Conn) { Type: controlFrameDeviceSnapshot, Version: controlProtocolVersion, }, controlDeviceSnapshot{ - Devices: []DeviceInfoV2{deviceInfoV2FromEntry(s.entry, "darwin-fake", "darwin-fake:"+s.entry.Info.BusIDString(), deviceStateAvailable, 0, "available")}, + Devices: []ControlDeviceInfo{controlDeviceInfoFromEntry(s.entry, "darwin-fake", "darwin-fake:"+s.entry.Info.BusIDString(), deviceStateAvailable, 0, "available")}, }) for { message, err := cr.read(conn) diff --git a/service/usbip/endpoint_darwin.go b/service/usbip/endpoint_darwin.go index 16de190df..ffd1eae65 100644 --- a/service/usbip/endpoint_darwin.go +++ b/service/usbip/endpoint_darwin.go @@ -202,8 +202,6 @@ func (e *darwinEndpoint) finalizePending(pending *pendingTransfer) { } } -// validateResponse reconciles a RET_SUBMIT against the original request shape. -// All wire-shape rules live here so accept can assume well-formed input. func (p *pendingTransfer) validateResponse(response SubmitResponse) (int32, error) { if response.ActualLength < 0 { return -int32(unix.EPROTO), E.New("RET_SUBMIT actual_length is negative: ", response.ActualLength) @@ -226,10 +224,7 @@ func (p *pendingTransfer) validateResponse(response SubmitResponse) (int32, erro return 0, nil } -// accept validates a RET_SUBMIT against the original request and, for IN -// transfers, scatters the payload into the Apple-owned buffer. A non-nil err -// signals a protocol violation; the caller is expected to cancel the endpoint -// so no further wire-corrupt completions are delivered to IOUSBHost. +// A non-nil err signals a protocol violation; the caller must cancel the endpoint. func (p *pendingTransfer) accept(response SubmitResponse) (int32, int, error) { errStatus, err := p.validateResponse(response) if err != nil { diff --git a/service/usbip/engine_darwin.go b/service/usbip/engine_darwin.go index bc10fb77e..28fe5ebce 100644 --- a/service/usbip/engine_darwin.go +++ b/service/usbip/engine_darwin.go @@ -2,10 +2,7 @@ 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. +// Close is a no-op: darwinExportHost owns the device handle across attachments. type darwinIOUSBHostEngine struct { device *darwinUSBHostDevice } diff --git a/service/usbip/engine_windows.go b/service/usbip/engine_windows.go index 8194c6f27..4aed9ff2f 100644 --- a/service/usbip/engine_windows.go +++ b/service/usbip/engine_windows.go @@ -3,15 +3,10 @@ package usbip import ( - "errors" - "github.com/sagernet/sing-box/common/vboxusb" E "github.com/sagernet/sing/common/exceptions" ) -// vboxusbEngine adapts a vboxusb.Device into the URBEngine interface. -// It owns the device handle for the lifetime of one userspaceURBSession -// (closed via Close). type vboxusbEngine struct { device *vboxusb.Device } @@ -52,9 +47,6 @@ func (e *vboxusbEngine) Close() error { return e.device.Close() } -// trapStandardControl returns (response, true) when the EP0 transfer -// is one VBoxUSB requires us to translate. (zero, false) means the -// caller should proceed with a normal control SEND_URB. func (e *vboxusbEngine) trapStandardControl(command SubmitCommand) (URBResponse, bool) { bmRequestType := command.Setup[0] bRequest := command.Setup[1] @@ -147,10 +139,6 @@ func (e *vboxusbEngine) bulkSubmit(req URBRequest, transferType vboxusb.Transfer return resp } -// isoSubmit currently rejects iso transfers exceeding VBoxUSB's 8- -// packet-per-URB limit. usbipd-win splits these into multiple -// parallel SEND_URB calls sharing one pinned buffer; that splitter is -// a Phase C follow-up. Single-shot iso under 8 packets does work. func (e *vboxusbEngine) isoSubmit(req URBRequest) URBResponse { command := req.Command if len(command.IsoPackets) > vboxusb.MaxIsoPacketsPerURB { @@ -215,22 +203,16 @@ func flagsFromCommand(transferFlags int32, usbipDir uint32) vboxusb.TransferFlag return vboxusb.TransferFlagShortOK } -// classifyURBError returns (status, ok). ok=true means the URB completed -// (with possibly a non-zero device-level status); ok=false means a -// transport failure that the caller surfaces as URBResponse.Error. func classifyURBError(err error) (int32, bool) { if err == nil { return 0, true } - var statusErr *vboxusb.URBStatusError - if errors.As(err, &statusErr) { + if statusErr, isStatus := E.Cast[*vboxusb.URBStatusError](err); isStatus { return vboxusbStatusToUSBIP(statusErr.Code), true } return 0, false } -// vboxusbStatusToUSBIP maps USBSUP_ERROR onto the USBIP (Linux errno) -// wire status convention. func vboxusbStatusToUSBIP(code vboxusb.URBError) int32 { switch code { case vboxusb.URBOK: diff --git a/service/usbip/export_ledger.go b/service/usbip/export_ledger.go index 665e0bb54..61d792ab2 100644 --- a/service/usbip/export_ledger.go +++ b/service/usbip/export_ledger.go @@ -24,7 +24,7 @@ type exportLedger struct { broadcastAccess sync.Mutex nextSubID uint64 subs map[uint64]*exportSubscriber - state map[string]DeviceInfoV2 + state map[string]ControlDeviceInfo inventoryAccess sync.Mutex exports map[string]Export @@ -47,7 +47,7 @@ func newExportLedger(logger log.ContextLogger, now func() time.Time) *exportLedg logger: logger, now: now, subs: make(map[uint64]*exportSubscriber), - state: make(map[string]DeviceInfoV2), + state: make(map[string]ControlDeviceInfo), exports: make(map[string]Export), busy: make(map[string]bool), } @@ -70,8 +70,6 @@ func (l *exportLedger) withInventoryRead(body func()) { body() } -// withInventoryWriteQuiet is for mutations whose broadcast is the -// caller's responsibility (paired with BroadcastIfChanged or shutdown). func (l *exportLedger) withInventoryWriteQuiet(body func()) { l.inventoryAccess.Lock() defer l.inventoryAccess.Unlock() @@ -115,22 +113,22 @@ func (l *exportLedger) ApplyHostSnapshot(snapshot map[string]Export, released [] } func (l *exportLedger) SeedBroadcastState() { - nextState := deviceInfoV2Map(l.snapshotDeviceState()) + nextState := controlDeviceInfoMap(l.snapshotDeviceState()) l.broadcastAccess.Lock() l.state = nextState l.broadcastAccess.Unlock() } func (l *exportLedger) BroadcastIfChanged() bool { - nextState := deviceInfoV2Map(l.snapshotDeviceState()) + nextState := controlDeviceInfoMap(l.snapshotDeviceState()) l.broadcastAccess.Lock() - if maps.EqualFunc(l.state, nextState, deviceInfoV2Equal) { + if maps.EqualFunc(l.state, nextState, controlDeviceInfoEqual) { l.broadcastAccess.Unlock() return false } l.state = nextState - devices := sortedDeviceInfoV2Values(nextState) + devices := sortedControlDeviceInfoValues(nextState) targets := make([]*exportSubscriber, 0, len(l.subs)) for _, sub := range l.subs { targets = append(targets, sub) @@ -145,8 +143,7 @@ func (l *exportLedger) BroadcastIfChanged() bool { return true } -// TryReserveForImport atomically reserves an exported busid. The single -// critical section under inventoryAccess closes the TOCTOU window between +// The single critical section closes the TOCTOU window between // availability check and busy mark. Caller must pair success with // ReleaseImport and broadcast once the session is wired up. func (l *exportLedger) TryReserveForImport(busid string) (Export, bool, string) { @@ -201,21 +198,18 @@ func (l *exportLedger) Subscribe(conn net.Conn) *exportSubscriber { l.enqueuePayload(sub, controlFrame{ Type: controlFrameDeviceSnapshot, Version: controlProtocolVersion, - }, controlDeviceSnapshot{Devices: sortedDeviceInfoV2Values(l.state)}) + }, controlDeviceSnapshot{Devices: sortedControlDeviceInfoValues(l.state)}) l.subs[sub.id] = sub return sub } -// Unsubscribe leaves the subscriber's send channel for the GC to -// reclaim; the transport read loop has already exited. func (l *exportLedger) Unsubscribe(sub *exportSubscriber) { l.broadcastAccess.Lock() delete(l.subs, sub.id) l.broadcastAccess.Unlock() } -// CloseAllSubscribers returns the underlying connections so the caller -// can close them outside any lock. +// Returns connections to close outside the lock. func (l *exportLedger) CloseAllSubscribers() []net.Conn { l.broadcastAccess.Lock() conns := make([]net.Conn, 0, len(l.subs)) @@ -234,7 +228,7 @@ func (l *exportLedger) ResetForClose() { }) } -func (l *exportLedger) snapshotDeviceState() []DeviceInfoV2 { +func (l *exportLedger) snapshotDeviceState() []ControlDeviceInfo { type entry struct { export Export busy bool @@ -252,13 +246,13 @@ func (l *exportLedger) snapshotDeviceState() []DeviceInfoV2 { slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.export.BusID(), b.export.BusID()) }) - out := make([]DeviceInfoV2, 0, len(entries)) + out := make([]ControlDeviceInfo, 0, len(entries)) for _, e := range entries { snapshot := e.export.Snapshot(e.busy) if snapshot.State == deviceStateUnavailable && snapshot.Entry.Info.BusIDString() == "" { continue } - out = append(out, deviceInfoV2FromEntry(snapshot.Entry, snapshot.Backend, snapshot.StableID, snapshot.State, snapshot.RawStatus, snapshot.StatusReason)) + out = append(out, controlDeviceInfoFromEntry(snapshot.Entry, snapshot.Backend, snapshot.StableID, snapshot.State, snapshot.RawStatus, snapshot.StatusReason)) } return out } diff --git a/service/usbip/handoff_linux.go b/service/usbip/handoff_linux.go index 09a052aa9..cec64a27b 100644 --- a/service/usbip/handoff_linux.go +++ b/service/usbip/handoff_linux.go @@ -165,10 +165,6 @@ func (h *kernelHandoffSession) Start() error { } func (h *kernelHandoffSession) runDirect(ctx context.Context, logger log.ContextLogger, side string, busid string, file *os.File) { - if file == nil { - h.markDone(nil) - return - } closeFile := sync.OnceFunc(func() { _ = file.Close() }) diff --git a/service/usbip/host_darwin.go b/service/usbip/host_darwin.go index a3ace6def..296730d2b 100644 --- a/service/usbip/host_darwin.go +++ b/service/usbip/host_darwin.go @@ -254,10 +254,6 @@ func (h *darwinExportHost) snapshotSelf() map[string]Export { return snapshotDarwinExports(h.exports) } -// snapshotDarwinExports returns every tracked export, including stale -// ones, matching the ExportSnapshot contract: stale exports surface -// to the ledger so they broadcast as State: unavailable updates -// instead of disappearing. func snapshotDarwinExports(exports map[string]*darwinExport) map[string]Export { out := make(map[string]Export, len(exports)) for busid, exp := range exports { @@ -329,9 +325,6 @@ func (e *darwinExport) DeviceInfo() (DeviceInfoTruncated, error) { } func (e *darwinExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) { - if e.device == nil { - return nil, E.New("darwin export ", e.busid, " has no device handle") - } return newUserspaceURBSession(ctx, e.logger, conn, newDarwinIOUSBHostEngine(e.device)), nil } diff --git a/service/usbip/host_linux.go b/service/usbip/host_linux.go index fdcc989ef..4fcff99ff 100644 --- a/service/usbip/host_linux.go +++ b/service/usbip/host_linux.go @@ -100,16 +100,10 @@ func (i linuxExportIdentity) Equal(other linuxExportIdentity) bool { i.ConfigValue != other.ConfigValue || i.NumConfigs != other.NumConfigs || i.NumInterfaces != other.NumInterfaces || - i.Serial != other.Serial || - len(i.Interfaces) != len(other.Interfaces) { + i.Serial != other.Serial { return false } - for index := range i.Interfaces { - if i.Interfaces[index] != other.Interfaces[index] { - return false - } - } - return true + return slices.Equal(i.Interfaces, other.Interfaces) } type linuxExportHost struct { @@ -311,11 +305,7 @@ func (h *linuxExportHost) Reconcile(isReserved func(busid string) bool) (map[str var reconcileErrors []error for _, busid := range plan.toStale { - exp, found := committed[busid] - if !found { - continue - } - cloned := cloneLinuxExport(exp) + cloned := cloneLinuxExport(committed[busid]) cloned.stale = true committed[busid] = cloned } @@ -413,11 +403,6 @@ func (h *linuxExportHost) snapshotSelf() map[string]Export { return snapshotLinuxExports(h.exports) } -// snapshotLinuxExports returns every tracked export, including stale -// ones. The ledger treats stale entries as broadcastable State: -// unavailable updates via Export.Snapshot, which is what the -// ExportSnapshot contract requires; filtering here would surface a -// removed device instead of an updated one. func snapshotLinuxExports(exports map[string]*linuxExport) map[string]Export { out := make(map[string]Export, len(exports)) for busid, exp := range exports { @@ -427,9 +412,6 @@ func snapshotLinuxExports(exports map[string]*linuxExport) map[string]Export { } func cloneLinuxExport(exp *linuxExport) *linuxExport { - if exp == nil { - return nil - } clone := *exp clone.descriptor.Interfaces = slices.Clone(exp.descriptor.Interfaces) clone.identity.Interfaces = slices.Clone(exp.identity.Interfaces) diff --git a/service/usbip/host_windows.go b/service/usbip/host_windows.go index af23289db..d1cd89a7e 100644 --- a/service/usbip/host_windows.go +++ b/service/usbip/host_windows.go @@ -19,18 +19,10 @@ func newPlatformExportHost(ctx context.Context, logger log.ContextLogger, matche return newWindowsExportHost(ctx, logger, matches), nil } -// newPlatformImportHost: usbip-client is not yet implemented on -// Windows. A virtual host controller driver is required to drive -// imported devices into the local USB stack — that is a separate -// effort tracked outside this PR. func newPlatformImportHost(_ log.ContextLogger) (ImportHost, error) { return nil, E.New("usbip-client service is not yet implemented on Windows") } -// windowsExportHost manages one VBoxUSBMon handle and one filter id -// per actively-matched device. Reconcile is called by ServerService -// on a timer (Events delivers ticks because CM_Register_Notification -// is not yet wired up — see vboxusb/pnp_windows.go). type windowsExportHost struct { logger log.ContextLogger matches []option.USBIPDeviceMatch @@ -79,9 +71,7 @@ func (h *windowsExportHost) Start() error { return E.New("windows usbip: VBoxUSBMon major version ", major, " (need ", vboxusb.DriverMajorVersion, ")") } h.logger.Info("VBoxUSBMon ", major, ".", minor, " ready") - h.access.Lock() h.monitor = monitor - h.access.Unlock() return nil } @@ -112,10 +102,6 @@ func (h *windowsExportHost) Close() error { return nil } -// Events returns a channel that the server polls for topology changes. -// On Windows, CM_Register_Notification wiring is a Phase B follow-up; -// for now we tick on a fixed interval so Reconcile runs periodically. -// Once the notification hook lands, this becomes edge-triggered. func (h *windowsExportHost) Events() (<-chan struct{}, error) { ch := make(chan struct{}, 1) go func() { @@ -209,10 +195,6 @@ func (h *windowsExportHost) FinishImport(busid string) (bool, error) { _ = exp.device.Close() exp.device = nil } - // TODO(phase B follow-up): once vboxusb.RestartDevice is implemented, - // call it here (or CycleHubPort) so Windows re-binds the device's - // natural function driver. Until then, the device stays in VBoxUSB's - // captured state until physical unplug. return false, nil } @@ -226,10 +208,6 @@ func (h *windowsExportHost) snapshotSelf() map[string]Export { return out } -// installFilterLocked is called from Reconcile under the assumption -// the caller already holds (or does not need) the lock for this -// h.filters mutation — but we re-lock briefly to keep the mutation -// safe. func (h *windowsExportHost) installFilterLocked(monitor *vboxusb.Monitor, busid string, info vboxusb.USBDeviceInfo) { if monitor == nil { return @@ -266,8 +244,6 @@ func (h *windowsExportHost) removeFilterLocked(monitor *vboxusb.Monitor, busid s } } -// windowsExport carries the per-device state needed to satisfy the -// Export interface and to drive the claim sequence in NewServerDataSession. type windowsExport struct { info vboxusb.USBDeviceInfo entry DeviceEntry @@ -313,16 +289,6 @@ func (e *windowsExport) DeviceInfo() (DeviceInfoTruncated, error) { return e.entry.Info, nil } -// NewServerDataSession executes the VBoxUSB claim sequence: -// -// 1. Wait for the VBoxUSB-bound device interface to appear under -// GUID_CLASS_VBOXUSB. This only resolves if VBoxUSBMon's filter has -// already triggered (next PnP arrival after AddFilter); for devices -// already plugged in when sing-box started, the user must physically -// unplug/replug, or RestartDevice must complete (Phase B follow-up). -// 2. Open the device handle and verify driver version. -// 3. USB_CLAIM_DEVICE to take exclusive ownership. -// 4. Wrap as vboxusbEngine and hand to userspaceURBSession. func (e *windowsExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) { path, err := vboxusb.WaitForVBoxUSBInterface(e.info.InstanceID, 10*time.Second) if err != nil { diff --git a/service/usbip/iso_scheduler.go b/service/usbip/iso_scheduler.go index c52e3c2b3..da39fceb8 100644 --- a/service/usbip/iso_scheduler.go +++ b/service/usbip/iso_scheduler.go @@ -6,11 +6,6 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -// EncodeIsoSubmit fills the isochronous SUBMIT fields on base. When asap is -// true, the wire-level ASAP flag is set and StartFrame is zeroed. Otherwise -// RebaseFrame recovers the absolute frame number from the controller's -// monotonic counter and ciFrame's 8 bits, then StartFrame carries the low 32 -// bits across the wire. func EncodeIsoSubmit(currentFrame uint64, base SubmitCommand, ciFrame uint8, asap bool) SubmitCommand { if asap { base.TransferFlags |= usbipTransferFlagIsoASAP diff --git a/service/usbip/match.go b/service/usbip/match.go index 32897cd67..84d107ac7 100644 --- a/service/usbip/match.go +++ b/service/usbip/match.go @@ -34,9 +34,6 @@ func matches(m option.USBIPDeviceMatch, d DeviceKey) bool { return true } -// SelectMatches returns the indexes of keys that match at least one -// non-zero pattern. Indexes are deduplicated and returned in ascending -// order so callers iterate devices in a stable sequence. func SelectMatches(patterns []option.USBIPDeviceMatch, keys []DeviceKey) []int { if len(patterns) == 0 || len(keys) == 0 { return nil diff --git a/service/usbip/session_userspace.go b/service/usbip/session_userspace.go index d74a13bd2..f4030122b 100644 --- a/service/usbip/session_userspace.go +++ b/service/usbip/session_userspace.go @@ -18,11 +18,6 @@ import ( 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 diff --git a/service/usbip/sysfs_linux.go b/service/usbip/sysfs_linux.go index d3aa6dbfa..eafaf5a2d 100644 --- a/service/usbip/sysfs_linux.go +++ b/service/usbip/sysfs_linux.go @@ -66,9 +66,6 @@ func (d *sysfsDevice) toProtocol() DeviceInfoTruncated { return info } -// vhciStatusRecord is one row of /sys/devices/platform/vhci_hcd.0/status -// or status.N. The kernel emits globally unique port numbers across every -// status* file. type vhciStatusRecord struct { hub string port int @@ -180,10 +177,7 @@ func readUsbipStatus(busid string) (int, error) { return v, nil } -// finishImportStatusTimeout is the upper bound for waitForUsbipStatusCleared. -// It is a var (not const) so interop tests can shrink it without changing the -// polling cadence. -var finishImportStatusTimeout = 2 * time.Second +const finishImportStatusTimeout = 2 * time.Second const finishImportStatusPollInterval = 25 * time.Millisecond @@ -209,10 +203,6 @@ func waitForUsbipStatusCleared(ctx context.Context, busid string) { } } -// readPrimaryVHCIStatus reads every status* file under -// /sys/devices/platform/vhci_hcd.0 and concatenates the rows in lexical -// order. Port numbers are already globally unique across controllers — no -// remapping is needed. func readPrimaryVHCIStatus() ([]vhciStatusRecord, error) { matches, err := filepath.Glob(filepath.Join(sysVHCIControllerV0, "status*")) if err != nil { diff --git a/service/usbip/urb_engine.go b/service/usbip/urb_engine.go index da4ad2fc0..785c38305 100644 --- a/service/usbip/urb_engine.go +++ b/service/usbip/urb_engine.go @@ -2,25 +2,12 @@ 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. +// Submit is called from per-endpoint goroutines; the session serializes +// per-endpoint so the engine needs no cross-endpoint lock. 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. + // Idempotent. Close() error } diff --git a/service/usbip/usbhost_darwin.go b/service/usbip/usbhost_darwin.go index 186228844..6ace7ad01 100644 --- a/service/usbip/usbhost_darwin.go +++ b/service/usbip/usbhost_darwin.go @@ -21,15 +21,7 @@ import ( ) const ( - ciStatusSuccess = 1 - ciStatusOffline = 2 - ciStatusNotPermitted = 3 - ciStatusBadArgument = 4 - ciStatusTimeout = 5 - ciStatusNoResources = 6 - ciStatusEndpointStopped = 7 - ciStatusStallError = 11 - ciStatusError = 13 + ciStatusSuccess = 1 ciMsgControllerPowerOn = 0x10 ciMsgControllerPowerOff = 0x11 @@ -89,7 +81,6 @@ func (c cgoCallbackHandle) token() C.uintptr_t { return C.uintptr_t(c.handle) } -// deleteRaw rolls back a failed C-side create without a drain. func (c cgoCallbackHandle) deleteRaw() { c.handle.Delete() } @@ -171,7 +162,7 @@ func darwinWatchUSBHostDevices(callback func()) (*darwinUSBHostDeviceWatcher, er } func (w *darwinUSBHostDeviceWatcher) Close() { - if w == nil || w.handle == nil { + if w.handle == nil { return } w.callback.closeAfter(func() { @@ -192,7 +183,7 @@ func darwinCreateUSBHostController(controller *darwinVirtualController, portCoun } func (c *darwinUSBHostController) Close() { - if c == nil || c.handle == nil { + if c.handle == nil { return } c.callback.closeAfter(func() { @@ -239,7 +230,7 @@ func (c *darwinUSBHostController) createDeviceSM(message darwinCIMessage) (*darw } func (s *darwinUSBHostDeviceSM) Close() { - if s == nil || s.handle == nil { + if s.handle == nil { return } C.box_usbhost_device_sm_destroy(s.handle) @@ -275,7 +266,7 @@ func (c *darwinUSBHostController) createEndpointSM(message darwinCIMessage) (*da } func (s *darwinUSBHostEndpointSM) Close() { - if s == nil || s.handle == nil { + if s.handle == nil { return } C.box_usbhost_endpoint_sm_destroy(s.handle) @@ -403,7 +394,7 @@ func box_usbip_darwin_usb_event(ref C.uintptr_t) { } func (d *darwinUSBHostDevice) Close() { - if d == nil || d.handle == nil { + if d.handle == nil { return } C.box_usbhost_device_close(d.handle) @@ -411,9 +402,6 @@ func (d *darwinUSBHostDevice) Close() { } func (d *darwinUSBHostDevice) control(setup [8]byte, buffer []byte) (int32, int32, []byte, error) { - if d == nil || d.handle == nil { - return -int32(unix.ENODEV), 0, nil, E.New("IOUSBHostDevice control: closed") - } var actual C.size_t var status C.int32_t var errorPtr *C.char @@ -429,9 +417,6 @@ func (d *darwinUSBHostDevice) control(setup [8]byte, buffer []byte) (int32, int3 } func (d *darwinUSBHostDevice) io(endpoint uint8, buffer []byte) (int32, int32, []byte, error) { - if d == nil || d.handle == nil { - return -int32(unix.ENODEV), 0, nil, E.New("IOUSBHostPipe IO: closed") - } var actual C.size_t var status C.int32_t var errorPtr *C.char @@ -446,9 +431,6 @@ func (d *darwinUSBHostDevice) io(endpoint uint8, buffer []byte) (int32, int32, [ } func (d *darwinUSBHostDevice) iso(endpoint uint8, buffer []byte, startFrame int32, asap bool, packets []IsoPacketDescriptor) (int32, int32, []byte, []IsoPacketDescriptor, error) { - if d == nil || d.handle == nil { - return -int32(unix.ENODEV), 0, nil, nil, E.New("IOUSBHostPipe isochronous IO: closed") - } var actual C.size_t var status C.int32_t var errorPtr *C.char @@ -478,9 +460,6 @@ func (d *darwinUSBHostDevice) iso(endpoint uint8, buffer []byte, startFrame int3 } func (d *darwinUSBHostDevice) abortEndpoint(endpoint uint8) error { - if d == nil || d.handle == nil { - return nil - } var errorPtr *C.char if !bool(C.box_usbhost_device_abort_endpoint(d.handle, C.uint8_t(endpoint), &errorPtr)) { return darwinCError(errorPtr) @@ -574,9 +553,7 @@ func darwinIOReturnToUSBIPStatus(status int32) int32 { } } -// darwinUSBIPStatusToCIStatus maps USB/IP transfer completion status to the -// corresponding IOUSBHostCI completion status. This is only used for real -// transfer completion, not for EndpointPause-driven state machine events. +// Only for real transfer completion, not for EndpointPause-driven state machine events. func darwinUSBIPStatusToCIStatus(status int32) int { if status == 0 { return int(C.IOUSBHostCIMessageStatusSuccess)