receiver: close paired devices before dropping the handle
Receiver.close() nulled its own handle first and only then closed its paired devices. A paired device's feature_request() sends over the receiver's handle, so the device.cleanups callbacks that run inside Device.close() - release RGB SW control, restore onboard-profile mode - were issued with handle=None, failed, and were silently swallowed. On every clean Solaar quit a receiver-paired keyboard was left in host mode; on the G915 TKL that means dead F-keys until a power cycle (#3266). Wired devices were unaffected: Device.close() already runs cleanups before clearing its handle, per its own comment. Close the devices first, then drop the handle, mirroring Device.close(). Also log cleanup write failures in rgb_power.cleanup at debug level instead of swallowing them silently, so a torn-down transport can't hide; debug because failures are routine when the device is unplugged. The regression test fails against the previous ordering.
This commit is contained in:
parent
5e4ae7261e
commit
5376bde14f
|
|
@ -189,11 +189,12 @@ class Receiver:
|
|||
def close(self):
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("%s: closing - handle %s %s", self, type(self.handle), self.handle)
|
||||
handle, self.handle = self.handle, None
|
||||
# Close paired devices first: their cleanup writes go over this receiver's handle.
|
||||
for _n, d in self._devices.items():
|
||||
if d:
|
||||
d.close()
|
||||
self._devices.clear()
|
||||
handle, self.handle = self.handle, None
|
||||
return handle and self.low_level.close(handle)
|
||||
|
||||
def __del__(self):
|
||||
|
|
|
|||
|
|
@ -338,8 +338,8 @@ def cleanup(device):
|
|||
device.feature_request(SupportedFeature.PROFILE_MANAGEMENT, 0x60, b"\x03")
|
||||
elif device.features and SupportedFeature.ONBOARD_PROFILES in device.features:
|
||||
device.feature_request(SupportedFeature.ONBOARD_PROFILES, 0x10, b"\x01")
|
||||
except Exception:
|
||||
pass # Device may already be offline
|
||||
except Exception as e:
|
||||
logger.debug("%s: cleanup release writes failed (device offline?): %s", device, e)
|
||||
if getattr(device, "_rgb_has_shutdown_cap", False):
|
||||
try:
|
||||
# SetRgbPowerMode(set=1, mode=0) — firmware off transition.
|
||||
|
|
|
|||
|
|
@ -369,3 +369,21 @@ def test_force_unpair_slot_no_handle():
|
|||
|
||||
assert r.force_unpair_slot(2) is False
|
||||
r._unpair_device_per_receiver.assert_not_called()
|
||||
|
||||
|
||||
def test_close_keeps_handle_until_paired_devices_closed():
|
||||
"""Paired-device cleanup writes go over the receiver's handle, so
|
||||
Receiver.close must close its devices before dropping it."""
|
||||
device_info = DeviceInfo("11")
|
||||
mock_low_level = LowLevelInterfaceFake(responses_unifying)
|
||||
r = receiver.create_receiver(mock_low_level, device_info, lambda x: x)
|
||||
handle_at_device_close = []
|
||||
dev = mock.Mock()
|
||||
dev.close.side_effect = lambda: handle_at_device_close.append(r.handle)
|
||||
r._devices[1] = dev
|
||||
|
||||
r.close()
|
||||
|
||||
assert handle_at_device_close == [fake_hidpp.open_path("11")]
|
||||
assert r.handle is None
|
||||
assert r._devices == {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue