From 40fab963fded4cb413019b556d38554d82e22fcd Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 24 Jul 2024 12:18:17 +0200 Subject: [PATCH 1/8] q-dev: port --- qubesguidaemon/mic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index ae340e7..4262789 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -31,10 +31,13 @@ class MicDevice(qubes.device_protocol.DeviceInfo): """Microphone device info class""" def __init__(self, backend_domain, product, manufacturer): - super().__init__( + port = qubes.device_protocol.Port( backend_domain=backend_domain, - ident="mic", - devclass="mic", + port_id="mic", + devclass="mic" + ) + super().__init__( + port, product=product, manufacturer=manufacturer, ) From 751733cff556a589413db8d6a20bb7b37dc1baa2 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 19 Aug 2024 07:52:15 +0200 Subject: [PATCH 2/8] q-dev: cleanup --- qubesguidaemon/mic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 4262789..7de7bf0 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -22,16 +22,16 @@ import subprocess -import qubes.device_protocol import qubes.ext import qubes.vm.adminvm +from qubes.device_protocol import Port, DeviceInterface, DeviceInfo -class MicDevice(qubes.device_protocol.DeviceInfo): +class MicDevice(DeviceInfo): """Microphone device info class""" def __init__(self, backend_domain, product, manufacturer): - port = qubes.device_protocol.Port( + port = Port( backend_domain=backend_domain, port_id="mic", devclass="mic" @@ -42,7 +42,7 @@ def __init__(self, backend_domain, product, manufacturer): manufacturer=manufacturer, ) self._interfaces = [ - qubes.device_protocol.DeviceInterface("******", devclass="mic") + DeviceInterface("******", devclass="mic") ] From deee25b601cef8a411131b5fa8baec35ec68263b Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 26 Aug 2024 12:26:08 +0200 Subject: [PATCH 3/8] q-dev: lint --- qubesguidaemon/mic.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 7de7bf0..702e668 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -32,18 +32,14 @@ class MicDevice(DeviceInfo): def __init__(self, backend_domain, product, manufacturer): port = Port( - backend_domain=backend_domain, - port_id="mic", - devclass="mic" + backend_domain=backend_domain, port_id="mic", devclass="mic" ) super().__init__( port, product=product, manufacturer=manufacturer, ) - self._interfaces = [ - DeviceInterface("******", devclass="mic") - ] + self._interfaces = [DeviceInterface("******", devclass="mic")] class MicDeviceExtension(qubes.ext.Extension): From 41d9badb414b986639a3eb0b6beaddecb74fa372 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 26 Sep 2024 14:51:08 +0200 Subject: [PATCH 4/8] q-dev: update argument name ident -> port_id --- qubesguidaemon/mic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 702e668..68eea5b 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -66,7 +66,7 @@ def on_device_list_mic(self, vm, event): return self.on_device_get_mic(vm, event, "mic") @qubes.ext.handler("device-get:mic") - def on_device_get_mic(self, vm, event, ident): + def on_device_get_mic(self, vm, event, port_id): """Get microphone device Currently, this assumes audio being handled in dom0. When adding support @@ -77,7 +77,7 @@ def on_device_get_mic(self, vm, event, ident): if not isinstance(vm, qubes.vm.adminvm.AdminVM): return - if ident != "mic": + if port_id != "mic": return yield self.get_device(vm.app) From 9008af813ee1b4b9f757e6c7e0c52ce760da6fa0 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 26 Sep 2024 15:07:04 +0200 Subject: [PATCH 5/8] q-dev: update argument name device -> port --- qubesguidaemon/mic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 68eea5b..7cec23e 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -144,11 +144,11 @@ async def on_device_pre_attach_mic(self, vm, event, device, options): # pylint: disable=unused-argument @qubes.ext.handler("device-pre-detach:mic") - async def on_device_pre_detach_mic(self, vm, event, device): + async def on_device_pre_detach_mic(self, vm, event, port): """Detach microphone from the VM""" # there is only one microphone - assert device == self.get_device(vm.app) + assert port == self.get_device(vm.app).port audiovm = getattr(vm, "audiovm", None) From 602a4c8085b0cdfdf947f9ad27fdffbf5766f06b Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 8 Oct 2024 11:40:08 +0200 Subject: [PATCH 6/8] q-dev: add less confusing device_id of microphone --- qubesguidaemon/mic.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 7cec23e..7d43e59 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -39,7 +39,14 @@ def __init__(self, backend_domain, product, manufacturer): product=product, manufacturer=manufacturer, ) - self._interfaces = [DeviceInterface("******", devclass="mic")] + self._interfaces = [DeviceInterface("000000", devclass="mic")] + + @property + def device_id(self) -> str: + """ + Get identification of a device not related to port. + """ + return "dom0:mic::m000000" class MicDeviceExtension(qubes.ext.Extension): From 43d85da7e084a6d619a71aff9d97142850fda465 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 8 Oct 2024 23:30:12 +0200 Subject: [PATCH 7/8] q-dev: mic auto-attach --- qubesguidaemon/mic.py | 61 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 7d43e59..7a62822 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -19,8 +19,9 @@ # with this program; if not, see . """Microphone control extension""" - +import asyncio import subprocess +import sys import qubes.ext import qubes.vm.adminvm @@ -116,7 +117,7 @@ async def on_device_pre_attach_mic(self, vm, event, device, options): assert device == self.get_device(vm.app) if options: raise qubes.exc.QubesException( - "mic device does not support options" + 'Microphone assignment does not support user options' ) audiovm = getattr(vm, "audiovm", None) @@ -187,6 +188,14 @@ async def on_device_pre_detach_mic(self, vm, event, port): "/audio-input-config/{}".format(vm.name), "0" ) + @qubes.ext.handler('device-pre-assign:mic') + async def on_device_assign_mic(self, vm, event, device, options): + # pylint: disable=unused-argument + + if options: + raise qubes.exc.QubesException( + 'Microphone assignment does not support user options') + @qubes.ext.handler("property-set:audiovm") def on_property_set(self, subject, event, name, newvalue, oldvalue=None): if not subject.is_running() or not newvalue: @@ -237,3 +246,51 @@ def on_domain_qdb_create(self, vm, event): vm.audiovm.untrusted_qdb.rm( "/audio-input-request/{}".format(vm.name) ) + + async def attach_and_notify(self, vm, assignment): + # bypass DeviceCollection logic preventing double attach + device = assignment.device + if assignment.mode.value == "ask-to-attach": + allowed = await qubes.ext.utils.confirm_device_attachment( + device, {vm: assignment}) + allowed = allowed.strip() + if vm.name != allowed: + return + await self.on_device_pre_attach_mic( + vm, 'device-pre-attach:mic', device, assignment.options) + await vm.fire_event_async( + 'device-attach:mic', device=device, options=assignment.options) + + @qubes.ext.handler('domain-start') + async def on_domain_start(self, vm, _event, **_kwargs): + # pylint: disable=unused-argument + to_attach = {} + assignments = vm.devices['mic'].get_assigned_devices() + # the most specific assignments first + for assignment in reversed(sorted(assignments)): + for device in assignment.devices: + if isinstance(device, qubes.device_protocol.UnknownDevice): + continue + if device.attachment: + continue + if not assignment.matches(device): + print( + "Unrecognized identity, skipping attachment of device " + f"from the port {assignment}", file=sys.stderr) + continue + # chose first assignment (the most specific) and ignore rest + if device not in to_attach: + # make it unique + to_attach[device] = assignment.clone(device=device) + for assignment in to_attach.values(): + asyncio.ensure_future(self.attach_and_notify(vm, assignment)) + + @qubes.ext.handler('domain-shutdown') + async def on_domain_shutdown(self, vm, _event, **_kwargs): + # pylint: disable=unused-argument + mic = self.get_device(vm.app) + if mic in vm.devices['mic'].get_attached_devices(): + asyncio.ensure_future(vm.fire_event_async( + f'device-detach:mic', port=mic.port + )) + From c11aa04f5b1bc637e24cd5b8f8626e5bddab80d7 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 17 Oct 2024 13:51:04 +0200 Subject: [PATCH 8/8] q-dev: cleanup mic.py --- qubesguidaemon/mic.py | 110 ++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 57 deletions(-) diff --git a/qubesguidaemon/mic.py b/qubesguidaemon/mic.py index 7a62822..e35338b 100644 --- a/qubesguidaemon/mic.py +++ b/qubesguidaemon/mic.py @@ -31,6 +31,8 @@ class MicDevice(DeviceInfo): """Microphone device info class""" + # pylint: disable=too-few-public-methods) + def __init__(self, backend_domain, product, manufacturer): port = Port( backend_domain=backend_domain, port_id="mic", devclass="mic" @@ -55,9 +57,6 @@ class MicDeviceExtension(qubes.ext.Extension): Extension to control microphone access """ - def __init__(self): - super(MicDeviceExtension, self).__init__() - @staticmethod def get_device(app): return MicDevice( @@ -80,7 +79,7 @@ def on_device_get_mic(self, vm, event, port_id): Currently, this assumes audio being handled in dom0. When adding support for GUI domain, this needs to be changed """ - # pylint: disable=unused-argument,no-self-use + # pylint: disable=unused-argument if not isinstance(vm, qubes.vm.adminvm.AdminVM): return @@ -93,6 +92,7 @@ def on_device_get_mic(self, vm, event, port_id): @qubes.ext.handler("device-list-attached:mic") def on_device_list_attached_mic(self, vm, event, persistent=None): """List attached microphone to the VM""" + # pylint: disable=unused-argument if persistent is True: return @@ -103,7 +103,7 @@ def on_device_list_attached_mic(self, vm, event, persistent=None): return untrusted_audio_input = audiovm.untrusted_qdb.read( - "/audio-input-config/{}".format(vm.name) + f"/audio-input-config/{vm.name}" ) if untrusted_audio_input == b"1": # (device, options) @@ -112,24 +112,23 @@ def on_device_list_attached_mic(self, vm, event, persistent=None): @qubes.ext.handler("device-pre-attach:mic") async def on_device_pre_attach_mic(self, vm, event, device, options): """Attach microphone to the VM""" + # pylint: disable=unused-argument # there is only one microphone assert device == self.get_device(vm.app) if options: raise qubes.exc.QubesException( - 'Microphone assignment does not support user options' + "Microphone assignment does not support user options" ) audiovm = getattr(vm, "audiovm", None) if audiovm is None: - raise qubes.exc.QubesException( - "VM {} has no AudioVM set".format(vm) - ) + raise qubes.exc.QubesException(f"VM {vm} has no AudioVM set") if not audiovm.is_running(): raise qubes.exc.QubesVMNotRunningError( - audiovm, "Audio VM {} isn't running".format(audiovm) + audiovm, f"Audio VM {audiovm} isn't running" ) if audiovm.features.check_with_netvm( @@ -137,18 +136,17 @@ async def on_device_pre_attach_mic(self, vm, event, device, options): ): try: await audiovm.run_service_for_stdio( - "qubes.AudioInputEnable+{}".format(vm.name) + f"qubes.AudioInputEnable+{vm.name}" ) except subprocess.CalledProcessError: + # pylint: disable=raise-missing-from raise qubes.exc.QubesVMError( vm, - "Failed to attach audio input from {!s} to {!s}: " - "pulseaudio agent not running".format(audiovm, vm), + f"Failed to attach audio input from {audiovm} to {vm}: " + "pulseaudio agent not running", ) else: - audiovm.untrusted_qdb.write( - "/audio-input-config/{}".format(vm.name), "1" - ) + audiovm.untrusted_qdb.write(f"/audio-input-config/{vm.name}", "1") # pylint: disable=unused-argument @qubes.ext.handler("device-pre-detach:mic") @@ -161,13 +159,11 @@ async def on_device_pre_detach_mic(self, vm, event, port): audiovm = getattr(vm, "audiovm", None) if audiovm is None: - raise qubes.exc.QubesException( - "VM {} has no AudioVM set".format(vm) - ) + raise qubes.exc.QubesException(f"VM {vm} has no AudioVM set") if not audiovm.is_running(): raise qubes.exc.QubesVMNotRunningError( - audiovm, "Audio VM {} isn't running".format(audiovm) + audiovm, f"Audio VM {audiovm} isn't running" ) if audiovm.features.check_with_netvm( @@ -175,97 +171,96 @@ async def on_device_pre_detach_mic(self, vm, event, port): ): try: await audiovm.run_service_for_stdio( - "qubes.AudioInputDisable+{}".format(vm.name) + f"qubes.AudioInputDisable+{vm.name}" ) except subprocess.CalledProcessError: + # pylint: disable=raise-missing-from raise qubes.exc.QubesVMError( vm, - "Failed to detach audio input from {!s} to {!s}: " - "pulseaudio agent not running".format(audiovm, vm), + f"Failed to detach audio input from {audiovm} to {vm}: " + "pulseaudio agent not running", ) else: - audiovm.untrusted_qdb.write( - "/audio-input-config/{}".format(vm.name), "0" - ) + audiovm.untrusted_qdb.write(f"/audio-input-config/{vm.name}", "0") - @qubes.ext.handler('device-pre-assign:mic') + @qubes.ext.handler("device-pre-assign:mic") async def on_device_assign_mic(self, vm, event, device, options): # pylint: disable=unused-argument if options: raise qubes.exc.QubesException( - 'Microphone assignment does not support user options') + "Microphone assignment does not support user options" + ) @qubes.ext.handler("property-set:audiovm") def on_property_set(self, subject, event, name, newvalue, oldvalue=None): + # pylint: disable=too-many-arguments if not subject.is_running() or not newvalue: return if not newvalue.is_running(): subject.log.warning( - "Cannot attach mic to {!s}: " - "AudioVM '{!s}' is powered off.".format(subject, newvalue) + f"Cannot attach mic to {subject}: " + f"AudioVM '{newvalue}' is powered off." ) if newvalue == oldvalue: return if oldvalue and oldvalue.is_running(): mic_allowed = oldvalue.untrusted_qdb.read( - "/audio-input-config/{}".format(subject.name) + f"/audio-input-config/{subject.name}" ) if mic_allowed is None: return try: mic_allowed_value = mic_allowed.decode("ascii") except UnicodeError: + # pylint: disable=raise-missing-from raise qubes.exc.QubesVMError( subject, - "Cannot decode ASCII value for '/audio-input-config/{!s}'".format( - subject.name - ), + f"Cannot decode ASCII value for " + f"'/audio-input-config/{subject.name}'", ) if mic_allowed_value in ("0", "1"): newvalue.untrusted_qdb.write( - "/audio-input-config/{}".format(subject.name), + f"/audio-input-config/{subject.name}", mic_allowed_value, ) else: raise qubes.exc.QubesVMError( subject, - "Invalid value '{!s}' for '/audio-input-config/{!s}' from {!s}".format( - mic_allowed_value, subject.name, oldvalue - ), + f"Invalid value '{mic_allowed_value}' for " + f"'/audio-input-config/{subject.name}' from {oldvalue}", ) @qubes.ext.handler("domain-qdb-create") def on_domain_qdb_create(self, vm, event): if vm.audiovm and vm.audiovm.is_running(): # Remove previous config, status and request entries on audiovm start - vm.audiovm.untrusted_qdb.rm( - "/audio-input-config/{}".format(vm.name) - ) - vm.audiovm.untrusted_qdb.rm("/audio-input/{}".format(vm.name)) - vm.audiovm.untrusted_qdb.rm( - "/audio-input-request/{}".format(vm.name) - ) + vm.audiovm.untrusted_qdb.rm(f"/audio-input-config/{vm.name}") + vm.audiovm.untrusted_qdb.rm(f"/audio-input/{vm.name}") + vm.audiovm.untrusted_qdb.rm(f"/audio-input-request/{vm.name}") async def attach_and_notify(self, vm, assignment): # bypass DeviceCollection logic preventing double attach device = assignment.device if assignment.mode.value == "ask-to-attach": allowed = await qubes.ext.utils.confirm_device_attachment( - device, {vm: assignment}) + device, {vm: assignment} + ) allowed = allowed.strip() if vm.name != allowed: return await self.on_device_pre_attach_mic( - vm, 'device-pre-attach:mic', device, assignment.options) + vm, "device-pre-attach:mic", device, assignment.options + ) await vm.fire_event_async( - 'device-attach:mic', device=device, options=assignment.options) + "device-attach:mic", device=device, options=assignment.options + ) - @qubes.ext.handler('domain-start') + @qubes.ext.handler("domain-start") async def on_domain_start(self, vm, _event, **_kwargs): # pylint: disable=unused-argument to_attach = {} - assignments = vm.devices['mic'].get_assigned_devices() + assignments = vm.devices["mic"].get_assigned_devices() # the most specific assignments first for assignment in reversed(sorted(assignments)): for device in assignment.devices: @@ -276,7 +271,9 @@ async def on_domain_start(self, vm, _event, **_kwargs): if not assignment.matches(device): print( "Unrecognized identity, skipping attachment of device " - f"from the port {assignment}", file=sys.stderr) + f"from the port {assignment}", + file=sys.stderr, + ) continue # chose first assignment (the most specific) and ignore rest if device not in to_attach: @@ -285,12 +282,11 @@ async def on_domain_start(self, vm, _event, **_kwargs): for assignment in to_attach.values(): asyncio.ensure_future(self.attach_and_notify(vm, assignment)) - @qubes.ext.handler('domain-shutdown') + @qubes.ext.handler("domain-shutdown") async def on_domain_shutdown(self, vm, _event, **_kwargs): # pylint: disable=unused-argument mic = self.get_device(vm.app) - if mic in vm.devices['mic'].get_attached_devices(): - asyncio.ensure_future(vm.fire_event_async( - f'device-detach:mic', port=mic.port - )) - + if mic in vm.devices["mic"].get_attached_devices(): + asyncio.ensure_future( + vm.fire_event_async("device-detach:mic", port=mic.port) + )