mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-03 22:24:44 +00:00
usbip: restart device nodes so VBoxUSB capture and release take effect
VBoxUSBMon only rewrites a device's IDs while PnP enumerates it, so adding a filter for an already-plugged device captured nothing until a physical replug, and releasing one left it dead under VBoxUSB.sys. Capture and release now drive the cfgmgr32 restart sequence (query-and-remove, hub port cycle, re-setup) around the filter change, mirroring usbipd-win's RestartingDevice. Capture also changes the devnode's identity to the VBox stub ID, which broke everything keyed on it: enumeration now reads the true vendor/product/class/speed from the parent hub's descriptor cache (the registry hardware ID reads as 80EE:CAFE once captured), data sessions locate the VBoxUSB interface by bus/address instead of the original instance ID, and Reconcile keeps exports alive through the re-enumeration window with a short absence grace.
This commit is contained in:
parent
5da1096a91
commit
bb60072e01
4 changed files with 436 additions and 68 deletions
|
|
@ -27,10 +27,24 @@ var USBDeviceInterfaceGUID = windows.GUID{
|
|||
Data4: [8]byte{0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed},
|
||||
}
|
||||
|
||||
// vboxStubVendorID/vboxStubProductID are the IDs VBoxUSBMon rewrites a
|
||||
// captured device's hardware ID to (so VBoxUSB.inf binds VBoxUSB.sys).
|
||||
// Their presence marks a device currently owned by VBoxUSB; the true
|
||||
// identity is then only available from the parent hub's descriptor.
|
||||
const (
|
||||
vboxStubVendorID uint16 = 0x80EE
|
||||
vboxStubProductID uint16 = 0xCAFE
|
||||
)
|
||||
|
||||
// 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 ("<bus>-<address>")
|
||||
// matching Linux usbip conventions.
|
||||
//
|
||||
// VendorID/ProductID/Revision/DeviceClass come from the parent hub's
|
||||
// cached device descriptor when available (stable across VBoxUSB
|
||||
// capture), falling back to the registry hardware ID (which reads as
|
||||
// the VBox stub ID once captured).
|
||||
type USBDeviceInfo struct {
|
||||
InstanceID string
|
||||
HardwareID string
|
||||
|
|
@ -42,6 +56,14 @@ type USBDeviceInfo struct {
|
|||
BusID string // "<bus>-<address>"
|
||||
DeviceClass uint8
|
||||
Speed DeviceSpeed
|
||||
Captured bool // currently owned by VBoxUSB.sys
|
||||
}
|
||||
|
||||
// IdentityIsStub reports whether VendorID/ProductID still carry the
|
||||
// VBoxUSB stub identity, i.e. the device is captured and the hub
|
||||
// descriptor (the only source of the true identity) was unavailable.
|
||||
func (i USBDeviceInfo) IdentityIsStub() bool {
|
||||
return i.VendorID == vboxStubVendorID && i.ProductID == vboxStubProductID
|
||||
}
|
||||
|
||||
// EnumerateUSBDevices walks GUID_DEVINTERFACE_USB_DEVICE and returns
|
||||
|
|
@ -93,31 +115,84 @@ func EnumerateUSBDevices() ([]USBDeviceInfo, error) {
|
|||
info.Address = toUint32(addressValue)
|
||||
}
|
||||
info.BusID = strconv.FormatUint(uint64(info.BusNumber), 10) + "-" + strconv.FormatUint(uint64(info.Address), 10)
|
||||
info.Speed = probe.speedOf(devInfo, data, info.Address)
|
||||
info.Captured = info.VendorID == vboxStubVendorID && info.ProductID == vboxStubProductID
|
||||
descriptor, speed := probe.describe(devInfo, data, info.Address)
|
||||
info.Speed = speed
|
||||
if descriptor != nil {
|
||||
info.VendorID = descriptor.vendorID
|
||||
info.ProductID = descriptor.productID
|
||||
info.Revision = descriptor.bcdDevice
|
||||
info.DeviceClass = descriptor.deviceClass
|
||||
}
|
||||
out = append(out, info)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func WaitForVBoxUSBInterface(instanceID string, timeout time.Duration) (string, error) {
|
||||
guid := MonitorAccessGUID
|
||||
// WaitForCapturedDevice polls for a VBoxUSB-owned device at the given
|
||||
// bus location and returns its VBoxUSB interface path. Capture changes
|
||||
// the device's instance ID (VBoxUSBMon rewrites it to the stub ID), so
|
||||
// the location — which survives the rewrite — is the only stable key.
|
||||
func WaitForCapturedDevice(busNumber, address uint32, timeout time.Duration) (string, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT)
|
||||
path, err := findCapturedDevice(busNumber, address)
|
||||
if err == nil {
|
||||
for _, p := range paths {
|
||||
if p != "" {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return "", E.New("vboxusb: VBoxUSB interface for ", instanceID, " did not appear within ", timeout)
|
||||
return "", E.Cause(err, "vboxusb: VBoxUSB interface for ", busNumber, "-", address, " did not appear within ", timeout)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func findCapturedDevice(busNumber, address uint32) (string, error) {
|
||||
guid := MonitorAccessGUID
|
||||
devInfo, err := windows.SetupDiGetClassDevsEx(
|
||||
&guid,
|
||||
"",
|
||||
0,
|
||||
windows.DIGCF_PRESENT|windows.DIGCF_DEVICEINTERFACE,
|
||||
0,
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "vboxusb: SetupDiGetClassDevsEx(VBoxUSB)")
|
||||
}
|
||||
defer devInfo.Close()
|
||||
for i := 0; ; i++ {
|
||||
data, err := windows.SetupDiEnumDeviceInfo(devInfo, i)
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
return "", E.New("vboxusb: no captured device at ", busNumber, "-", address)
|
||||
}
|
||||
return "", E.Cause(err, "vboxusb: SetupDiEnumDeviceInfo[", i, "]")
|
||||
}
|
||||
busNumberValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_BUSNUMBER)
|
||||
if err != nil || toUint32(busNumberValue) != busNumber {
|
||||
continue
|
||||
}
|
||||
addressValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_ADDRESS)
|
||||
if err != nil || toUint32(addressValue) != address {
|
||||
continue
|
||||
}
|
||||
instanceID, err := windows.SetupDiGetDeviceInstanceId(devInfo, data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, p := range paths {
|
||||
if p != "" {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func firstString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
|
|
|
|||
156
common/vboxusb/restart_windows.go
Normal file
156
common/vboxusb/restart_windows.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
//go:build windows
|
||||
|
||||
package vboxusb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// DeviceRestart drives the cfgmgr32 restart-device sequence VBoxUSBMon
|
||||
// depends on: the monitor only rewrites a device's IDs (and thereby
|
||||
// hands it to VBoxUSB.sys, or back to its function driver) while PnP
|
||||
// re-enumerates the device, which never happens for a device that is
|
||||
// already sitting configured on the bus. Begin removes the devnode
|
||||
// without restarting it; the caller mutates filters in between; Finish
|
||||
// cycles the hub port (a software unplug/replug, so drivers see a
|
||||
// clean device) and re-enables the devnode.
|
||||
//
|
||||
// Mirrors usbipd-win's RestartingDevice.
|
||||
type DeviceRestart struct {
|
||||
devInst uint32
|
||||
hubPath string
|
||||
port uint32
|
||||
}
|
||||
|
||||
const (
|
||||
cmLocateDevNodeNormal = 0x00000000
|
||||
cmRemoveUINotOK = 0x00000001
|
||||
cmRemoveNoRestart = 0x00000002
|
||||
cmSetupDevNodeReady = 0x00000000
|
||||
maxDeviceIDLength = 200
|
||||
ioctlUSBHubCyclePort = 0x0022_0444 // CTL_CODE(FILE_DEVICE_USB, USB_HUB_CYCLE_PORT=273, METHOD_BUFFERED, FILE_ANY_ACCESS)
|
||||
deviceRestartSettleTime = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// BeginDeviceRestart resolves the devnode and its parent hub, then
|
||||
// removes the device subtree without restart. Call Finish to bring the
|
||||
// device back; the pair must not be left half-open.
|
||||
func BeginDeviceRestart(instanceID string, port uint32) (*DeviceRestart, error) {
|
||||
instanceW, err := windows.UTF16PtrFromString(instanceID)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "vboxusb: utf16 instance id")
|
||||
}
|
||||
var devInst uint32
|
||||
ret, _, _ := procCMLocateDevNodeW.Call(
|
||||
uintptr(unsafe.Pointer(&devInst)),
|
||||
uintptr(unsafe.Pointer(instanceW)),
|
||||
cmLocateDevNodeNormal,
|
||||
)
|
||||
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
|
||||
return nil, E.New("vboxusb: CM_Locate_DevNode(", instanceID, ") CR=", ret)
|
||||
}
|
||||
hubPath := parentHubInterfacePath(devInst)
|
||||
var vetoType uint32
|
||||
var vetoName [260]uint16
|
||||
ret, _, _ = procCMQueryAndRemoveSubTreeW.Call(
|
||||
uintptr(devInst),
|
||||
uintptr(unsafe.Pointer(&vetoType)),
|
||||
uintptr(unsafe.Pointer(&vetoName[0])),
|
||||
uintptr(len(vetoName)),
|
||||
cmRemoveNoRestart|cmRemoveUINotOK,
|
||||
)
|
||||
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
|
||||
return nil, E.New("vboxusb: CM_Query_And_Remove_SubTree(", instanceID, ") CR=", ret,
|
||||
" veto=", vetoType, " by ", windows.UTF16ToString(vetoName[:]))
|
||||
}
|
||||
return &DeviceRestart{devInst: devInst, hubPath: hubPath, port: port}, nil
|
||||
}
|
||||
|
||||
// Finish re-enumerates the removed device. Errors are swallowed by
|
||||
// design (mirrors upstream): the device may have been physically
|
||||
// unplugged meanwhile, or re-enumerated by someone else already.
|
||||
func (r *DeviceRestart) Finish() {
|
||||
// Give the just-switched driver stack time to settle; upstream
|
||||
// found flash drives fail to re-initialize without this.
|
||||
time.Sleep(deviceRestartSettleTime)
|
||||
cycleHubPort(r.hubPath, r.port)
|
||||
_, _, _ = procCMSetupDevNode.Call(uintptr(r.devInst), cmSetupDevNodeReady)
|
||||
}
|
||||
|
||||
// parentHubInterfacePath resolves the USB hub interface path of the
|
||||
// device's parent before removal (afterwards the parent link may be
|
||||
// unreliable). Empty on failure; Finish then skips the port cycle.
|
||||
func parentHubInterfacePath(devInst uint32) string {
|
||||
var parent uint32
|
||||
ret, _, _ := procCMGetParent.Call(
|
||||
uintptr(unsafe.Pointer(&parent)),
|
||||
uintptr(devInst),
|
||||
0,
|
||||
)
|
||||
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
|
||||
return ""
|
||||
}
|
||||
var parentID [maxDeviceIDLength + 1]uint16
|
||||
ret, _, _ = procCMGetDeviceIDW.Call(
|
||||
uintptr(parent),
|
||||
uintptr(unsafe.Pointer(&parentID[0])),
|
||||
uintptr(len(parentID)),
|
||||
0,
|
||||
)
|
||||
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
|
||||
return ""
|
||||
}
|
||||
paths, err := windows.CM_Get_Device_Interface_List(
|
||||
windows.UTF16ToString(parentID[:]),
|
||||
&usbHubInterfaceGUID,
|
||||
windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT,
|
||||
)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, p := range paths {
|
||||
if p != "" {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// cycleHubPort issues IOCTL_USB_HUB_CYCLE_PORT — a software
|
||||
// unplug/replug of the given port. Best effort.
|
||||
func cycleHubPort(hubPath string, port uint32) {
|
||||
if hubPath == "" || port == 0 {
|
||||
return
|
||||
}
|
||||
hub := openHub(hubPath)
|
||||
if hub == windows.InvalidHandle {
|
||||
return
|
||||
}
|
||||
defer windows.CloseHandle(hub)
|
||||
// USB_CYCLE_PORT_PARAMS: ConnectionIndex (in) + StatusReturned (out).
|
||||
var params [8]byte
|
||||
binary.LittleEndian.PutUint32(params[0:4], port)
|
||||
var returned uint32
|
||||
_ = windows.DeviceIoControl(
|
||||
hub,
|
||||
ioctlUSBHubCyclePort,
|
||||
¶ms[0], uint32(len(params)),
|
||||
¶ms[0], uint32(len(params)),
|
||||
&returned, nil,
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
modCfgMgr32 = windows.NewLazySystemDLL("cfgmgr32.dll")
|
||||
procCMLocateDevNodeW = modCfgMgr32.NewProc("CM_Locate_DevNodeW")
|
||||
procCMGetParent = modCfgMgr32.NewProc("CM_Get_Parent")
|
||||
procCMGetDeviceIDW = modCfgMgr32.NewProc("CM_Get_Device_IDW")
|
||||
procCMQueryAndRemoveSubTreeW = modCfgMgr32.NewProc("CM_Query_And_Remove_SubTreeW")
|
||||
procCMSetupDevNode = modCfgMgr32.NewProc("CM_Setup_DevNode")
|
||||
)
|
||||
|
|
@ -69,13 +69,25 @@ const (
|
|||
nodeConnInfoExV2FlagSuperSpeedPlus = 0x4
|
||||
)
|
||||
|
||||
// hubSpeedProbe resolves the negotiated link speed of devices by querying their
|
||||
// parent hub. Open hub handles are cached for the lifetime of one enumeration;
|
||||
// a nil/InvalidHandle entry caches a failure so it is not retried per device.
|
||||
// hubSpeedProbe resolves devices' negotiated link speed and cached device
|
||||
// descriptor by querying their parent hub. Open hub handles are cached for
|
||||
// the lifetime of one enumeration; a nil/InvalidHandle entry caches a
|
||||
// failure so it is not retried per device.
|
||||
type hubSpeedProbe struct {
|
||||
hubs map[string]windows.Handle
|
||||
}
|
||||
|
||||
// hubDeviceDescriptor carries the identity fields of the
|
||||
// USB_DEVICE_DESCRIPTOR embedded in USB_NODE_CONNECTION_INFORMATION_EX.
|
||||
// The hub reports the real descriptor regardless of which function
|
||||
// driver owns the device, so these survive VBoxUSB capture.
|
||||
type hubDeviceDescriptor struct {
|
||||
vendorID uint16
|
||||
productID uint16
|
||||
bcdDevice uint16
|
||||
deviceClass uint8
|
||||
}
|
||||
|
||||
func newHubSpeedProbe() *hubSpeedProbe {
|
||||
return &hubSpeedProbe{hubs: make(map[string]windows.Handle)}
|
||||
}
|
||||
|
|
@ -89,15 +101,16 @@ func (p *hubSpeedProbe) close() {
|
|||
p.hubs = nil
|
||||
}
|
||||
|
||||
// speedOf returns the device's link speed, or SpeedUnknown if the parent hub
|
||||
// could not be opened or did not answer. port is the hub port index
|
||||
// (SPDRP_ADDRESS) the device is attached to.
|
||||
func (p *hubSpeedProbe) speedOf(devInfo windows.DevInfo, data *windows.DevInfoData, port uint32) DeviceSpeed {
|
||||
// describe returns the device's descriptor identity and link speed, or
|
||||
// (nil, SpeedUnknown) if the parent hub could not be opened or did not
|
||||
// answer. port is the hub port index (SPDRP_ADDRESS) the device is
|
||||
// attached to.
|
||||
func (p *hubSpeedProbe) describe(devInfo windows.DevInfo, data *windows.DevInfoData, port uint32) (*hubDeviceDescriptor, DeviceSpeed) {
|
||||
hub := p.parentHub(devInfo, data)
|
||||
if hub == windows.InvalidHandle {
|
||||
return SpeedUnknown
|
||||
return nil, SpeedUnknown
|
||||
}
|
||||
return querySpeed(hub, port)
|
||||
return queryNodeConnection(hub, port)
|
||||
}
|
||||
|
||||
func (p *hubSpeedProbe) parentHub(devInfo windows.DevInfo, data *windows.DevInfoData) windows.Handle {
|
||||
|
|
@ -144,7 +157,7 @@ func openHub(hubPath string) windows.Handle {
|
|||
return handle
|
||||
}
|
||||
|
||||
func querySpeed(hub windows.Handle, port uint32) DeviceSpeed {
|
||||
func queryNodeConnection(hub windows.Handle, port uint32) (*hubDeviceDescriptor, DeviceSpeed) {
|
||||
buffer := make([]byte, nodeConnInfoExBufferSize)
|
||||
binary.LittleEndian.PutUint32(buffer[0:4], port)
|
||||
var returned uint32
|
||||
|
|
@ -156,23 +169,34 @@ func querySpeed(hub windows.Handle, port uint32) DeviceSpeed {
|
|||
&returned, nil,
|
||||
)
|
||||
if err != nil || returned <= nodeConnInfoExSpeedOffset {
|
||||
return SpeedUnknown
|
||||
return nil, SpeedUnknown
|
||||
}
|
||||
// USB_DEVICE_DESCRIPTOR starts at offset 4 (after ConnectionIndex):
|
||||
// bDeviceClass at +4, idVendor at +8, idProduct at +10, bcdDevice at +12.
|
||||
descriptor := &hubDeviceDescriptor{
|
||||
deviceClass: buffer[8],
|
||||
vendorID: binary.LittleEndian.Uint16(buffer[12:14]),
|
||||
productID: binary.LittleEndian.Uint16(buffer[14:16]),
|
||||
bcdDevice: binary.LittleEndian.Uint16(buffer[16:18]),
|
||||
}
|
||||
var speed DeviceSpeed
|
||||
switch buffer[nodeConnInfoExSpeedOffset] {
|
||||
case usbDeviceSpeedLow:
|
||||
return SpeedLow
|
||||
speed = SpeedLow
|
||||
case usbDeviceSpeedFull:
|
||||
return SpeedFull
|
||||
speed = SpeedFull
|
||||
case usbDeviceSpeedHigh:
|
||||
return SpeedHigh
|
||||
speed = SpeedHigh
|
||||
case usbDeviceSpeedSuper:
|
||||
if superSpeedPlus(hub, port) {
|
||||
return SpeedSuperPlus
|
||||
speed = SpeedSuperPlus
|
||||
} else {
|
||||
speed = SpeedSuper
|
||||
}
|
||||
return SpeedSuper
|
||||
default:
|
||||
return SpeedUnknown
|
||||
speed = SpeedUnknown
|
||||
}
|
||||
return descriptor, speed
|
||||
}
|
||||
|
||||
func superSpeedPlus(hub windows.Handle, port uint32) bool {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ func newPlatformImportHost(logger log.ContextLogger) (ImportHost, error) {
|
|||
return &windowsImportHost{logger: logger}, nil
|
||||
}
|
||||
|
||||
// windowsExportAbsenceGrace covers the window in which a device under
|
||||
// capture/release restart is removed from the PnP tree and therefore
|
||||
// missing from enumeration. Exports seen more recently than this are
|
||||
// not dropped just because the device is momentarily absent.
|
||||
const windowsExportAbsenceGrace = 10 * time.Second
|
||||
|
||||
type windowsExportHost struct {
|
||||
logger log.ContextLogger
|
||||
matches []option.USBIPDeviceMatch
|
||||
|
|
@ -85,6 +91,33 @@ func (h *windowsExportHost) Close() error {
|
|||
exports := h.exports
|
||||
h.exports = make(map[string]*windowsExport)
|
||||
h.access.Unlock()
|
||||
for busid, exp := range exports {
|
||||
device := exp.takeDevice()
|
||||
if device != nil {
|
||||
_ = device.Close()
|
||||
}
|
||||
instanceID, captured := exp.lastState()
|
||||
var restart *vboxusb.DeviceRestart
|
||||
if captured {
|
||||
var err error
|
||||
restart, err = vboxusb.BeginDeviceRestart(instanceID, exp.info.Address)
|
||||
if err != nil {
|
||||
h.logger.Warn("restart ", busid, " for release: ", err)
|
||||
}
|
||||
}
|
||||
if monitor != nil {
|
||||
if id, ok := filters[busid]; ok {
|
||||
delete(filters, busid)
|
||||
err := monitor.RemoveFilter(id)
|
||||
if err != nil {
|
||||
h.logger.Debug("remove filter for ", busid, ": ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if restart != nil {
|
||||
restart.Finish()
|
||||
}
|
||||
}
|
||||
if monitor != nil {
|
||||
for busid, id := range filters {
|
||||
err := monitor.RemoveFilter(id)
|
||||
|
|
@ -94,12 +127,6 @@ func (h *windowsExportHost) Close() error {
|
|||
}
|
||||
_ = monitor.Close()
|
||||
}
|
||||
for _, exp := range exports {
|
||||
device := exp.takeDevice()
|
||||
if device != nil {
|
||||
_ = device.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -129,23 +156,6 @@ func (h *windowsExportHost) Reconcile(isReserved func(busid string) bool) (map[s
|
|||
if err != nil {
|
||||
return h.snapshotSelf(), nil, E.Cause(err, "windows usbip: enumerate USB devices")
|
||||
}
|
||||
keys := make([]DeviceKey, 0, len(devices))
|
||||
for _, d := range devices {
|
||||
keys = append(keys, DeviceKey{
|
||||
BusID: d.BusID,
|
||||
VendorID: d.VendorID,
|
||||
ProductID: d.ProductID,
|
||||
})
|
||||
}
|
||||
desired := make(map[string]vboxusb.USBDeviceInfo)
|
||||
for _, idx := range SelectMatches(h.matches, keys) {
|
||||
info := devices[idx]
|
||||
if info.DeviceClass == 0x09 {
|
||||
h.logger.Warn("skip hub device ", info.BusID)
|
||||
continue
|
||||
}
|
||||
desired[info.BusID] = info
|
||||
}
|
||||
|
||||
h.access.Lock()
|
||||
monitor := h.monitor
|
||||
|
|
@ -155,24 +165,67 @@ func (h *windowsExportHost) Reconcile(isReserved func(busid string) bool) (map[s
|
|||
}
|
||||
h.access.Unlock()
|
||||
|
||||
present := make(map[string]vboxusb.USBDeviceInfo, len(devices))
|
||||
keys := make([]DeviceKey, 0, len(devices))
|
||||
for _, d := range devices {
|
||||
present[d.BusID] = d
|
||||
key := DeviceKey{
|
||||
BusID: d.BusID,
|
||||
VendorID: d.VendorID,
|
||||
ProductID: d.ProductID,
|
||||
}
|
||||
if exp, ok := current[d.BusID]; ok && d.Captured && d.IdentityIsStub() {
|
||||
// The hub descriptor was unavailable and the registry reports
|
||||
// the VBox stub ID; evaluate the match against the identity
|
||||
// recorded when the export was created.
|
||||
key.VendorID = exp.info.VendorID
|
||||
key.ProductID = exp.info.ProductID
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
desired := make(map[string]vboxusb.USBDeviceInfo)
|
||||
for _, idx := range SelectMatches(h.matches, keys) {
|
||||
info := present[keys[idx].BusID]
|
||||
if info.DeviceClass == 0x09 {
|
||||
h.logger.Warn("skip hub device ", info.BusID)
|
||||
continue
|
||||
}
|
||||
if info.Captured {
|
||||
if _, exported := current[info.BusID]; !exported {
|
||||
h.logger.Warn("device ", info.BusID, " is captured by VBoxUSB outside this service; replug it to export")
|
||||
continue
|
||||
}
|
||||
}
|
||||
desired[info.BusID] = info
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var released []string
|
||||
for busid, info := range desired {
|
||||
if _, ok := current[busid]; ok {
|
||||
if exp, ok := current[busid]; ok {
|
||||
exp.markSeen(now, info.InstanceID, info.Captured)
|
||||
continue
|
||||
}
|
||||
exp := newWindowsExport(info, h.logger)
|
||||
h.installFilterLocked(monitor, busid, info)
|
||||
exp.markSeen(now, info.InstanceID, info.Captured)
|
||||
h.captureDevice(monitor, busid, info)
|
||||
current[busid] = exp
|
||||
h.logger.Info("matched ", busid, " (vid=", fmt.Sprintf("0x%04x", info.VendorID), " pid=", fmt.Sprintf("0x%04x", info.ProductID), ") — capture pending PnP arrival")
|
||||
h.logger.Info("matched ", busid, " (vid=", fmt.Sprintf("0x%04x", info.VendorID), " pid=", fmt.Sprintf("0x%04x", info.ProductID), ") — capturing")
|
||||
}
|
||||
for busid := range current {
|
||||
for busid, exp := range current {
|
||||
if _, ok := desired[busid]; ok {
|
||||
continue
|
||||
}
|
||||
if isReserved(busid) {
|
||||
continue
|
||||
}
|
||||
h.removeFilterLocked(monitor, busid)
|
||||
info, isPresent := present[busid]
|
||||
if !isPresent && now.Sub(exp.seenAt()) < windowsExportAbsenceGrace {
|
||||
// Likely mid-restart: the devnode is removed while PnP
|
||||
// re-enumerates it. Keep the export until the grace passes.
|
||||
continue
|
||||
}
|
||||
h.releaseDevice(monitor, busid, exp, info, isPresent)
|
||||
delete(current, busid)
|
||||
released = append(released, busid)
|
||||
h.logger.Info("released ", busid, " (no longer matches)")
|
||||
|
|
@ -209,10 +262,19 @@ func (h *windowsExportHost) snapshotSelf() map[string]Export {
|
|||
return out
|
||||
}
|
||||
|
||||
func (h *windowsExportHost) installFilterLocked(monitor *vboxusb.Monitor, busid string, info vboxusb.USBDeviceInfo) {
|
||||
// captureDevice installs a capture filter and forces the device through
|
||||
// PnP re-enumeration. VBoxUSBMon only rewrites a device's IDs (handing
|
||||
// it to VBoxUSB.sys) while the device enumerates, so without the
|
||||
// restart an already-plugged device would keep its function driver
|
||||
// until physically replugged.
|
||||
func (h *windowsExportHost) captureDevice(monitor *vboxusb.Monitor, busid string, info vboxusb.USBDeviceInfo) {
|
||||
if monitor == nil {
|
||||
return
|
||||
}
|
||||
restart, err := vboxusb.BeginDeviceRestart(info.InstanceID, info.Address)
|
||||
if err != nil {
|
||||
h.logger.Warn("restart ", busid, " for capture: ", err)
|
||||
}
|
||||
vendor := info.VendorID
|
||||
product := info.ProductID
|
||||
filterID, err := monitor.AddFilter(vboxusb.Filter{
|
||||
|
|
@ -221,14 +283,40 @@ func (h *windowsExportHost) installFilterLocked(monitor *vboxusb.Monitor, busid
|
|||
})
|
||||
if err != nil {
|
||||
h.logger.Warn("ADD_FILTER for ", busid, ": ", err)
|
||||
return
|
||||
} else {
|
||||
h.access.Lock()
|
||||
h.filters[busid] = filterID
|
||||
h.access.Unlock()
|
||||
}
|
||||
if restart != nil {
|
||||
restart.Finish()
|
||||
}
|
||||
h.access.Lock()
|
||||
h.filters[busid] = filterID
|
||||
h.access.Unlock()
|
||||
}
|
||||
|
||||
func (h *windowsExportHost) removeFilterLocked(monitor *vboxusb.Monitor, busid string) {
|
||||
// releaseDevice removes the capture filter and, if the device is still
|
||||
// present and captured, restarts it so its original function driver
|
||||
// re-binds. Without the restart the device stays dead to Windows until
|
||||
// physically replugged.
|
||||
func (h *windowsExportHost) releaseDevice(monitor *vboxusb.Monitor, busid string, exp *windowsExport, info vboxusb.USBDeviceInfo, isPresent bool) {
|
||||
device := exp.takeDevice()
|
||||
if device != nil {
|
||||
_ = device.Close()
|
||||
}
|
||||
var restart *vboxusb.DeviceRestart
|
||||
if isPresent && info.Captured {
|
||||
var err error
|
||||
restart, err = vboxusb.BeginDeviceRestart(info.InstanceID, info.Address)
|
||||
if err != nil {
|
||||
h.logger.Warn("restart ", busid, " for release: ", err)
|
||||
}
|
||||
}
|
||||
h.removeFilter(monitor, busid)
|
||||
if restart != nil {
|
||||
restart.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *windowsExportHost) removeFilter(monitor *vboxusb.Monitor, busid string) {
|
||||
if monitor == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -250,28 +338,53 @@ type windowsExport struct {
|
|||
entry DeviceEntry
|
||||
logger log.ContextLogger
|
||||
|
||||
deviceAccess sync.Mutex
|
||||
device *vboxusb.Device
|
||||
stateAccess sync.Mutex
|
||||
device *vboxusb.Device
|
||||
lastSeen time.Time
|
||||
currentInstanceID string
|
||||
seenCaptured bool
|
||||
}
|
||||
|
||||
// setDevice records the claimed handle once NewServerDataSession opens it.
|
||||
func (e *windowsExport) setDevice(device *vboxusb.Device) {
|
||||
e.deviceAccess.Lock()
|
||||
e.stateAccess.Lock()
|
||||
e.device = device
|
||||
e.deviceAccess.Unlock()
|
||||
e.stateAccess.Unlock()
|
||||
}
|
||||
|
||||
// takeDevice atomically hands the claimed handle to exactly one caller and
|
||||
// clears the field, so FinishImport and Close racing on shutdown cannot both
|
||||
// close the same handle.
|
||||
func (e *windowsExport) takeDevice() *vboxusb.Device {
|
||||
e.deviceAccess.Lock()
|
||||
e.stateAccess.Lock()
|
||||
device := e.device
|
||||
e.device = nil
|
||||
e.deviceAccess.Unlock()
|
||||
e.stateAccess.Unlock()
|
||||
return device
|
||||
}
|
||||
|
||||
// markSeen records the device's enumeration state. The instance ID must be
|
||||
// re-tracked every pass because capture rewrites it to the VBox stub ID.
|
||||
func (e *windowsExport) markSeen(now time.Time, instanceID string, captured bool) {
|
||||
e.stateAccess.Lock()
|
||||
e.lastSeen = now
|
||||
e.currentInstanceID = instanceID
|
||||
e.seenCaptured = captured
|
||||
e.stateAccess.Unlock()
|
||||
}
|
||||
|
||||
func (e *windowsExport) seenAt() time.Time {
|
||||
e.stateAccess.Lock()
|
||||
defer e.stateAccess.Unlock()
|
||||
return e.lastSeen
|
||||
}
|
||||
|
||||
func (e *windowsExport) lastState() (string, bool) {
|
||||
e.stateAccess.Lock()
|
||||
defer e.stateAccess.Unlock()
|
||||
return e.currentInstanceID, e.seenCaptured
|
||||
}
|
||||
|
||||
func newWindowsExport(info vboxusb.USBDeviceInfo, logger log.ContextLogger) *windowsExport {
|
||||
entry := DeviceEntry{
|
||||
Info: DeviceInfoTruncated{
|
||||
|
|
@ -329,7 +442,7 @@ func (e *windowsExport) DeviceInfo() (DeviceInfoTruncated, error) {
|
|||
}
|
||||
|
||||
func (e *windowsExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
|
||||
path, err := vboxusb.WaitForVBoxUSBInterface(e.info.InstanceID, 10*time.Second)
|
||||
path, err := vboxusb.WaitForCapturedDevice(e.info.BusNumber, e.info.Address, 10*time.Second)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "windows usbip: locate VBoxUSB interface")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue