diff --git a/common/vboxusb/pnp_windows.go b/common/vboxusb/pnp_windows.go index 7238bee7a..14243bff6 100644 --- a/common/vboxusb/pnp_windows.go +++ b/common/vboxusb/pnp_windows.go @@ -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 ("-
") // 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 // "-
" 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: diff --git a/common/vboxusb/restart_windows.go b/common/vboxusb/restart_windows.go new file mode 100644 index 000000000..cee0dc4bf --- /dev/null +++ b/common/vboxusb/restart_windows.go @@ -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") +) diff --git a/common/vboxusb/speed_windows.go b/common/vboxusb/speed_windows.go index e8184aaf6..47a2f548e 100644 --- a/common/vboxusb/speed_windows.go +++ b/common/vboxusb/speed_windows.go @@ -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 { diff --git a/service/usbip/host_windows.go b/service/usbip/host_windows.go index cce8a4b3c..6030fe2ff 100644 --- a/service/usbip/host_windows.go +++ b/service/usbip/host_windows.go @@ -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") }