From 457854d4346adc6981e3d9dfabbaf4f49a96398f Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 7 Jul 2026 16:15:40 +0200 Subject: [PATCH] q-dev: mark device as busy if subdevice is used mirror the qubes-core-admin change. add serialization/deserialization of `busy` property so that devices with a child currently passed through to a VM can be listed without being offered for attachment. Absence of the key means available (not busy). --- qubesadmin/device_protocol.py | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/qubesadmin/device_protocol.py b/qubesadmin/device_protocol.py index 2e9dc494..cdb93f42 100644 --- a/qubesadmin/device_protocol.py +++ b/qubesadmin/device_protocol.py @@ -45,6 +45,29 @@ from qubesadmin.app import VMCollection +# duplicated from qubesadmin.utils to avoid an import cycle +def qbool(value): + """ + Property setter for boolean properties. + + It accepts (case-insensitive) ``'0'``, ``'no'`` and ``false`` as + :py:obj:`False` and ``'1'``, ``'yes'`` and ``'true'`` as + :py:obj:`True`. + """ + + if isinstance(value, str): + lcvalue = value.lower() + if lcvalue in ("0", "no", "false", "off"): + return False + if lcvalue in ("1", "yes", "true", "on"): + return True + raise QubesValueError( + "Invalid literal for boolean property: {!r}".format(value) + ) + + return bool(value) + + class ProtocolError(AssertionError): """ Raised when something is wrong with data received. @@ -908,6 +931,7 @@ def __init__( parent: DeviceInfo | None = None, attachment: QubesVM | None = None, device_id: str | None = None, + busy: bool | None = None, **kwargs, ): super().__init__(port, device_id) @@ -920,6 +944,7 @@ def __init__( self._interfaces = interfaces self._parent = parent self._attachment = attachment + self._busy = busy self.data = kwargs @@ -1074,6 +1099,17 @@ def attachment(self) -> QubesVM | None: """ return self._attachment + @property + def busy(self) -> bool: + """ + Is the device currently busy (unavailable for attachment)? + + True when a child device (partition or sub-device) is currently + passed through to a VM. The device remains visible but cannot be + attached until all children are detached. + """ + return bool(self._busy) + def serialize(self) -> bytes: """ Serialize an object to be transmitted via Qubes API. @@ -1097,6 +1133,11 @@ def serialize(self) -> bytes: "attachment", self.attachment.name ) + if self.busy: + properties += b" " + DeviceSerializer.pack_property( + "busy", "True" + ) + properties += b" " + DeviceSerializer.pack_property( "interfaces", "".join(repr(ifc) for ifc in self.interfaces) ) @@ -1180,6 +1221,14 @@ def _deserialize( del properties["parent_port_id"] del properties["parent_devclass"] + # A missing property means free; otherwise parse strictly. An + # invalid value raises QubesValueError, which the caller + # (deserialize) turns into an UnknownDevice. + untrusted_busy = properties.pop("busy", None) + properties["busy"] = ( + qbool(untrusted_busy) if untrusted_busy is not None else False + ) + return cls(**properties) @property