unpair: --dry-run is silently ignored without --slot and performs a real unpair
Summary
solaar unpair --dry-run <device> silently performs a real, forced unpair. The --dry-run flag is documented as applying only to --slot, but argparse accepts it alongside a positional device argument, where nothing reads it. The command then prints Unpaired N: ... — output indistinguishable from a genuine run — so there is no way to tell from the terminal that a write happened.
I lost two pairings this way while trying to preview which slots an unpair would clear.
Version
- Solaar 1.1.20 (Arch
solaar 1.1.20-2); code is identical onmasteras of today - Bolt receiver, 046d:C548, three slots paired to the same MX Master 3S
Reproduction
With a receiver that has a device paired in slot 1:
$ solaar unpair --dry-run 1
Unpaired 1: MX Master 3S (MX Master 3S) [B034:47206275]
$ solaar show | grep "paired device"
Has 2 paired device(s) out of a maximum of 6. # was 3The device is really gone.
Cause
In lib/solaar/cli/unpair.py, run() dispatches to _run_slot_unpair() only when --slot is given. The positional-device path below it never consults args.dry_run:
def run(receivers, args, find_receiver, find_device):
assert receivers
if getattr(args, "slot", None) is not None:
_run_slot_unpair(receivers, args, find_receiver)
return
assert args.device, "unpair requires a device name, or use --slot"
device_name = args.device.lower()
dev = next(find_device(receivers, device_name), None)
if not dev:
raise Exception(f"no device found matching '{device_name}'")
...
dev.receiver._unpair_device(number, True) # force an unpair
print(f"Unpaired {int(number)}: {dev.name} ({codename}) [{wpid}:{serial}]")_run_slot_unpair() handles the flag correctly:
if getattr(args, "dry_run", False):
print(f"[dry-run] would force-unpair slot {slot} — no register write issued")
returnSo the behavior is arguably as documented — lib/solaar/cli/__init__.py says "with --slot, run all safety checks but do not issue the unpair register write". The defect is that the flag is silently ignored rather than rejected when that precondition is not met, and the operation it fails to suppress is destructive and not undoable without physically re-pairing.
Why this is worse than it looks
--slot is gated to Lightspeed receivers:
if rcv.receiver_kind != "lightspeed":
raise Exception("--slot unpair is currently only supported on Lightspeed receivers ...")So on Bolt, Unifying, and Nano receivers there is no working dry-run path at all — yet --dry-run is accepted there without complaint, and the only form of the command those users can run is the one that ignores it. A user on a Bolt receiver reading solaar unpair --help sees a --dry-run option and has no indication it cannot apply to them.
Suggested fix
Reject the combination instead of ignoring it:
if getattr(args, "dry_run", False) and getattr(args, "slot", None) is None:
raise Exception("--dry-run is only supported with --slot")That turns a silent destructive write into an error, and is a two-line change.
Better still, if it is not too invasive: honor --dry-run on the device path too. Everything needed to report the intent is already gathered before the write — number, codename, wpid, serial are read immediately above _unpair_device, so the preview could print the same line prefixed with [dry-run] and return. That would give Bolt/Unifying/Nano users the safety check the flag advertises, rather than only an error explaining they cannot have it.
Happy to open a PR for either version.
Source: pwr-Solaar/Solaar