diff --git a/Makefile b/Makefile index 9d552f33..78adc9c2 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ install-icons: install-autostart: mkdir -p $(DESTDIR)/etc/xdg/autostart + cp autostart/qubes-device-agent.desktop $(DESTDIR)/etc/xdg/autostart cp autostart/qui-domains.desktop $(DESTDIR)/etc/xdg/autostart cp autostart/qui-devices.desktop $(DESTDIR)/etc/xdg/autostart cp autostart/qui-clipboard.desktop $(DESTDIR)/etc/xdg/autostart @@ -48,6 +49,8 @@ install-autostart: cp desktop/qubes-global-config.desktop $(DESTDIR)/usr/share/applications/ cp desktop/qubes-new-qube.desktop $(DESTDIR)/usr/share/applications/ cp desktop/qubes-policy-editor-gui.desktop $(DESTDIR)/usr/share/applications/ + install -d $(DESTDIR)/usr/lib/qubes -m 0755 + install -m 0755 qui/devices/qubes-device-agent-autostart $(DESTDIR)/usr/lib/qubes/qubes-device-agent-autostart install-lang: mkdir -p $(DESTDIR)/usr/share/gtksourceview-4/language-specs/ diff --git a/autostart/qubes-device-agent.desktop b/autostart/qubes-device-agent.desktop new file mode 100644 index 00000000..1cb9c9ec --- /dev/null +++ b/autostart/qubes-device-agent.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Qubes Device Agent +Comment=Agent for handling device attach confirmation prompts +Icon=qubes +Exec=/usr/lib/qubes/qubes-device-agent-autostart +Terminal=false +Type=Application diff --git a/qubes_config/tests/test_usb_devices.py b/qubes_config/tests/test_usb_devices.py index c766200c..40d73eb6 100644 --- a/qubes_config/tests/test_usb_devices.py +++ b/qubes_config/tests/test_usb_devices.py @@ -845,15 +845,13 @@ def test_u2f_handler_add_without_service(test_qapp, def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): test_qapp.expected_calls[('sys-usb', "admin.vm.device.pci.Attached", None, None)] = \ - b"0\x00dom0+00_0d.0 ident='00_0d.0' devclass='pci' " \ - b"backend_domain='dom0' required='yes' attach_automatically='yes' " \ + b"0\x00dom0+00_0d.0 device_id='*' port_id='00_0d.0' devclass='pci' " \ + b"backend_domain='dom0' mode='required' " \ b"_no-strict-reset='yes'\n" test_qapp.expected_calls[('dom0', "admin.vm.device.pci.Available", None, None)] = \ - b"0\x0000_0d.0 ident='00_0d.0' devclass='pci' backend_domain='dom0' " \ - b"serial='unknown' manufacturer='unknown' " \ - b"self_identity='0000:0000::p0c0300' vendor='unknown' " \ - b"product='unknown' name='unknown' interfaces='p0c0300' " \ + b"0\x0000_0d.0 device_id='0000:0000::p0c0300' port_id='00_0d.0' " \ + b"devclass='pci' backend_domain='dom0' interfaces='p0c0300' " \ b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " \ b"_device='0d'\n" @@ -878,26 +876,22 @@ def test_devices_handler_detect_usbvms(test_qapp, test_policy_manager, real_builder): test_qapp.expected_calls[('sys-usb', "admin.vm.device.pci.Attached", None, None)] = \ - b"0\x00dom0+00_0d.0 ident='00_0d.0' devclass='pci' " \ - b"backend_domain='dom0' required='yes' attach_automatically='yes' " \ + b"0\x00dom0+00_0d.0 device_id='*' port_id='00_0d.0' devclass='pci' " \ + b"backend_domain='dom0' mode='required' " \ b"_no-strict-reset='yes'\n" test_qapp.expected_calls[('test-standalone', "admin.vm.device.pci.Attached", None, None)] = \ - b"0\x00dom0+00_0f.0 ident='00_0f.0' devclass='pci' " \ - b"backend_domain='dom0' required='yes' attach_automatically='yes' " \ + b"0\x00dom0+00_0f.0 device_id='*' port_id='00_0f.0' devclass='pci' " \ + b"backend_domain='dom0' mode='required' " \ b"_no-strict-reset='yes'\n" test_qapp.expected_calls[('dom0', "admin.vm.device.pci.Available", None, None)] = \ - b"0\x0000_0f.0 ident='00_0f.0' devclass='pci' backend_domain='dom0' " \ - b"serial='unknown' manufacturer='unknown' " \ - b"self_identity='0000:0000::p0c0300' vendor='unknown' " \ - b"product='unknown' name='unknown' interfaces='p0c0300' " \ + b"0\x0000_0f.0 device_id='0000:0000::p0c0300' port_id='00_0f.0' " \ + b"devclass='pci' backend_domain='dom0' interfaces='p0c0300' " \ b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0f_0' " \ b"_device='0f'\n" \ - b"00_0d.0 ident='00_0d.0' devclass='pci' backend_domain='dom0' " \ - b"serial='unknown' manufacturer='unknown' " \ - b"self_identity='0000:0000::p0c0300' vendor='unknown' " \ - b"product='unknown' name='unknown' interfaces='p0c0300' " \ + b"00_0d.0 device_id='0000:0000::p0c0300' port_id='00_0d.0' " \ + b"devclass='pci' backend_domain='dom0' interfaces='p0c0300' " \ b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " \ b"_device='0d'\n" diff --git a/qui/decorators.py b/qui/decorators.py index 41b46491..8a76fb6a 100644 --- a/qui/decorators.py +++ b/qui/decorators.py @@ -247,7 +247,7 @@ def device_hbox(device) -> Gtk.Box: dev_icon = create_icon(icon) name_label = Gtk.Label(xalign=0) - name = f"{device.backend_domain}:{device.ident} - {device.description}" + name = f"{device.backend_domain}:{device.port_id} - {device.description}" if device.attachments: dev_list = ", ".join(list(device.attachments)) name_label.set_markup(f'{name} ({dev_list})') diff --git a/qui/devices/AttachConfirmationWindow.glade b/qui/devices/AttachConfirmationWindow.glade new file mode 100644 index 00000000..d8c0df42 --- /dev/null +++ b/qui/devices/AttachConfirmationWindow.glade @@ -0,0 +1,359 @@ + + + + + + 400 + False + Device attachment + center + dialog-question + dialog + True + center + + + True + False + vertical + + + True + False + True + error + True + + + False + 6 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + gtk-dialog-error + + + False + True + 0 + + + + + True + False + ErrorMessage + + + False + True + 1 + + + + + False + False + 0 + + + + + False + True + 0 + + + + + 100 + 80 + True + False + 12 + 12 + 12 + 12 + True + vertical + 6 + + + True + False + 6 + end + + + gtk-cancel + True + True + True + True + + + True + True + 0 + + + + + gtk-ok + True + False + True + True + True + True + + + True + True + 1 + + + + + False + True + end + 1 + + + + + True + False + vertical + 6 + + + True + False + 6 + 12 + + + True + False + gtk-dialog-question + 6 + + + False + True + 0 + + + + + True + False + start + Do you want to attach the following device? +<small>Select the target domain and confirm with 'OK'</small> + True + + + True + True + 1 + + + + + False + True + 0 + + + + + True + False + 12 + 6 + 12 + 6 + + + True + False + 1 + Target: + + + 0 + 2 + + + + + True + False + True + True + + + True + 5 + False + Start typing or use the arrow + GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE + + + + + 1 + 2 + + + + + True + False + 1 + Source: + + + 0 + 0 + + + + + True + False + False + source + False + + + 1 + 0 + + + + + True + False + 1 + Device: + + + 0 + 1 + + + + + True + False + 0 + qubes.<b>MyOperation</b> + True + + + 1 + 1 + + + + + False + True + 1 + + + + + True + True + + + True + False + 6 + vertical + 6 + + + Display templates in the target list + True + True + False + 0 + True + + + False + True + 0 + + + + + Choose a custom destination in the target + True + True + False + 0 + True + + + False + True + 1 + + + + + + + False + Advanced options + True + + + + + False + True + 2 + + + + + True + True + 2 + + + + + False + True + 1 + + + + + + diff --git a/qui/devices/backend.py b/qui/devices/backend.py index 449f7d21..b496a73e 100644 --- a/qui/devices/backend.py +++ b/qui/devices/backend.py @@ -24,6 +24,7 @@ import qubesadmin.devices import qubesadmin.vm from qubesadmin.utils import size_to_human +from qubesadmin.device_protocol import DeviceAssignment import gi gi.require_version('Gtk', '3.0') # isort:skip @@ -107,10 +108,11 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, if dev.devclass == 'block' and 'size' in dev.data: self._dev_name += " (" + size_to_human(int(dev.data['size'])) + ")" - self._ident: str = getattr(dev, 'ident', 'unknown') + self._ident: str = getattr(dev, 'port_id', 'unknown') self._description: str = getattr(dev, 'description', 'unknown') self._devclass: str = getattr(dev, 'devclass', 'unknown') self._data: Dict = getattr(dev, 'data', {}) + self._device_id = getattr(dev, 'device_id', '*') self.attachments: Set[VM] = set() backend_domain = getattr(dev, 'backend_domain', None) if backend_domain: @@ -231,8 +233,9 @@ def attach_to_vm(self, vm: VM): Perform attachment to provided VM. """ try: - assignment = qubesadmin.device_protocol.DeviceAssignment( - self.backend_domain, self.id_string) + assignment = DeviceAssignment.new(self.backend_domain, + port_id=self.id_string, devclass=self.device_class, + device_id=self._device_id) vm.vm_object.devices[self.device_class].attach(assignment) self.gtk_app.emit_notification( @@ -262,8 +265,8 @@ def detach_from_vm(self, vm: VM): Gio.NotificationPriority.NORMAL, notification_id=self.notification_id) try: - assignment = qubesadmin.device_protocol.DeviceAssignment( - self.backend_domain, self._ident) + assignment = DeviceAssignment.new( + self.backend_domain, self._ident, self.device_class) vm.vm_object.devices[self.device_class].detach(assignment) except qubesadmin.exc.QubesException as ex: self.gtk_app.emit_notification( diff --git a/qui/devices/device_widget.py b/qui/devices/device_widget.py index 44d0fd9a..942ae298 100644 --- a/qui/devices/device_widget.py +++ b/qui/devices/device_widget.py @@ -75,6 +75,7 @@ def __init__(self, app_name, qapp, dispatcher): super().__init__() self.name: str = app_name + # maps: port to connected device (e.g., sys-usb:sda -> block device) self.devices: Dict[str, backend.Device] = {} self.vms: Set[backend.VM] = set() self.dispvm_templates: Set[backend.VM] = set() @@ -124,15 +125,16 @@ def device_list_update(self, vm, _event, **_kwargs): try: for devclass in DEV_TYPES: for device in vm.devices[devclass]: - changed_devices[str(device)] = backend.Device(device, self) + changed_devices[str(device.port)] = backend.Device( + device, self) except qubesadmin.exc.QubesException: changed_devices = {} # VM was removed - for dev_name, dev in changed_devices.items(): - if dev_name not in self.devices: + for dev_port, dev in changed_devices.items(): + if dev_port not in self.devices: dev.connection_timestamp = time.monotonic() - self.devices[dev_name] = dev + self.devices[dev_port] = dev self.emit_notification( _("Device available"), _("Device {} is available.").format(dev.description), @@ -140,19 +142,19 @@ def device_list_update(self, vm, _event, **_kwargs): notification_id=dev.notification_id) dev_to_remove = [] - for dev_name, dev in self.devices.items(): + for dev_port, dev in self.devices.items(): if dev.backend_domain != vm: continue - if dev_name not in changed_devices: - dev_to_remove.append((dev_name, dev)) + if dev_port not in changed_devices: + dev_to_remove.append((dev_port, dev)) - for dev_name, dev in dev_to_remove: + for dev_port, dev in dev_to_remove: self.emit_notification( _("Device removed"), _("Device {} has been removed.").format(dev.description), Gio.NotificationPriority.NORMAL, notification_id=dev.notification_id) - del self.devices[dev_name] + del self.devices[dev_port] def initialize_vm_data(self): for vm in self.qapp.domains: @@ -172,7 +174,8 @@ def initialize_dev_data(self): for devclass in DEV_TYPES: try: for device in domain.devices[devclass]: - self.devices[str(device)] = backend.Device(device, self) + self.devices[str(device.port)] = backend.Device( + device, self) except qubesadmin.exc.QubesException: # we have no permission to access VM's devices continue @@ -183,7 +186,7 @@ def initialize_dev_data(self): try: for device in domain.devices[devclass ].get_attached_devices(): - dev = str(device) + dev = str(device.port) if dev in self.devices: # occassionally ghost UnknownDevices appear when a # device was removed but not detached from a VM @@ -202,14 +205,14 @@ def device_attached(self, vm, _event, device, **_kwargs): # we don't have access to VM state return - if str(device) not in self.devices: - self.devices[str(device)] = backend.Device(device, self) + if str(device.port) not in self.devices: + self.devices[str(device.port)] = backend.Device(device, self) vm_wrapped = backend.VM(vm) - self.devices[str(device)].attachments.add(vm_wrapped) + self.devices[str(device.port)].attachments.add(vm_wrapped) - def device_detached(self, vm, _event, device, **_kwargs): + def device_detached(self, vm, _event, port, **_kwargs): try: if not vm.is_running(): return @@ -217,11 +220,11 @@ def device_detached(self, vm, _event, device, **_kwargs): # we don't have access to VM state return - device = str(device) + port = str(port) vm_wrapped = backend.VM(vm) - if device in self.devices: - self.devices[device].attachments.discard(vm_wrapped) + if port in self.devices: + self.devices[port].attachments.discard(vm_wrapped) def vm_start(self, vm, _event, **_kwargs): wrapped_vm = backend.VM(vm) @@ -231,7 +234,7 @@ def vm_start(self, vm, _event, **_kwargs): for devclass in DEV_TYPES: try: for device in vm.devices[devclass].get_attached_devices(): - dev = str(device) + dev = str(device.port) if dev in self.devices: self.devices[dev].attachments.add(wrapped_vm) except qubesadmin.exc.QubesDaemonAccessError: diff --git a/qui/devices/qubes-device-agent-autostart b/qui/devices/qubes-device-agent-autostart new file mode 100755 index 00000000..8e001ce3 --- /dev/null +++ b/qui/devices/qubes-device-agent-autostart @@ -0,0 +1,9 @@ +#!/bin/sh + +if ! test -f /var/run/qubes-service/guivm && \ + ! test -f /etc/qubes-release; then + echo "Not in GuiVM or dom0. Exiting." + exit 0 +fi + +exec qubes-device-agent "$@" diff --git a/qui/tools/__init__.py b/qui/tools/__init__.py new file mode 100644 index 00000000..1d211911 --- /dev/null +++ b/qui/tools/__init__.py @@ -0,0 +1,20 @@ +# coding=utf-8 +# +# The Qubes OS Project, https://www.qubes-os.org +# +# Copyright (C) 2024 Piotr Bartman-Szwarc +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. diff --git a/qui/tools/qubes_device_agent.py b/qui/tools/qubes_device_agent.py new file mode 100644 index 00000000..0ef0c330 --- /dev/null +++ b/qui/tools/qubes_device_agent.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# +# The Qubes OS Project, https://www.qubes-os.org +# +# Copyright (C) 2024 Piotr Bartman-Szwarc +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. +""" Agent running in user session, responsible for asking the user about device +attachment.""" + +import os +import argparse +import asyncio + +import importlib.resources + +# pylint: disable=import-error,wrong-import-position +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +# pylint: enable=import-error + +# pylint: disable=wrong-import-order +import gbulb + +# pylint: enable=wrong-import-position + +from qrexec.server import SocketService +from qrexec.utils import sanitize_domain_name +from qrexec.tools.qrexec_policy_agent import ( + VMListModeler, RPCConfirmationWindow) +from qubesadmin.device_protocol import DeviceSerializer + + +DEVICE_AGENT_SOCKET_PATH = "/var/run/qubes/device-agent.GUI" + + +class VMAndPortListModeler(VMListModeler): + def __init__(self, options, domains_info=None): + super().__init__(domains_info) + self._override_entries(options) + + def _override_entries(self, options): + self._entries = {} + for name, vm in self._domains_info.items(): + if name.startswith("@dispvm:"): + vm_name = name[len("@dispvm:"):] + prefix = "Disposable VM: " + else: + vm_name = name + prefix = "" + sanitize_domain_name(vm_name, assert_sanitized=True) + + icon = self._get_icon(vm.get("icon", None)) + + display_name = prefix + vm_name + options.get(name, "") + display_name = display_name.strip() + self._entries[display_name] = { + "api_name": vm_name, + "icon": icon, + "vm": vm, + } + + def apply_icon(self, entry, qube_name): + if isinstance(entry, Gtk.Entry): + for vm_info in self._entries.values(): + if qube_name == vm_info['api_name']: + entry.set_icon_from_pixbuf( + Gtk.EntryIconPosition.PRIMARY, vm_info["icon"], + ) + break + else: + raise ValueError( + f"The following source qube does not exist: {qube_name}") + else: + raise TypeError( + "Only expecting Gtk.Entry objects to want our icon." + ) + + +class AttachmentConfirmationWindow(RPCConfirmationWindow): + # pylint: disable=too-few-public-methods,too-many-instance-attributes + _source_file_ref = importlib.resources.files("qui").joinpath( + os.path.join("devices", "AttachConfirmationWindow.glade")) + + _source_id = { + "window": "AttachConfirmationWindow", + "ok": "okButton", + "cancel": "cancelButton", + "source": "sourceEntry", + "device_label": "deviceLabel", + "target": "TargetCombo", + "error_bar": "ErrorBar", + "error_message": "ErrorMessage", + } + + # We reuse most parts of superclass, but we need custom init, + # so we DO NOT call super().__init__() + # pylint: disable=super-init-not-called + def __init__( + self, + entries_info, source, device_name, argument, targets_list, target=None + ): + # pylint: disable=too-many-arguments + sanitize_domain_name(source, assert_sanitized=True) + DeviceSerializer.sanitize_str( + device_name, DeviceSerializer.ALLOWED_CHARS_PARAM, + error_message="Invalid device name") + + self._gtk_builder = Gtk.Builder() + with importlib.resources.as_file(self._source_file_ref) as path: + self._gtk_builder.add_from_file(str(path)) + self._rpc_window = self._gtk_builder.get_object( + self._source_id["window"] + ) + self._rpc_ok_button = self._gtk_builder.get_object( + self._source_id["ok"] + ) + self._rpc_cancel_button = self._gtk_builder.get_object( + self._source_id["cancel"] + ) + self._device_label = self._gtk_builder.get_object( + self._source_id["device_label"] + ) + self._source_entry = self._gtk_builder.get_object( + self._source_id["source"] + ) + self._rpc_combo_box = self._gtk_builder.get_object( + self._source_id["target"] + ) + self._error_bar = self._gtk_builder.get_object( + self._source_id["error_bar"] + ) + self._error_message = self._gtk_builder.get_object( + self._source_id["error_message"] + ) + self._target_name = None + + self._focus_helper = self._new_focus_stealing_helper() + + self._device_label.set_markup(device_name) + + self._entries_info = entries_info + + options = {name: " " + options for vm_data in targets_list + for name, _, options in (vm_data.partition(" "),)} + list_modeler = self._new_vm_list_modeler_overridden(options) + + list_modeler.apply_model( + self._rpc_combo_box, + options.keys(), + selection_trigger=self._update_ok_button_sensitivity, + activation_trigger=self._clicked_ok, + ) + + self._source_entry.set_text(source + ":" + argument) + list_modeler.apply_icon(self._source_entry, source) + + self._confirmed = None + + self._set_initial_target(source, target) + + self._connect_events() + + def _new_vm_list_modeler_overridden(self, options): + return VMAndPortListModeler(options, self._entries_info) + + +async def confirm_attachment( + entries_info, source, device_name, argument, targets_list, target=None +): + # pylint: disable=too-many-arguments + window = AttachmentConfirmationWindow( + entries_info, source, device_name, argument, targets_list, target + ) + + return await window.confirm_rpc() + + +class DeviceAgent(SocketService): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._app = Gtk.Application() + self._app.set_application_id("qubes.device-agent") + self._app.register() + + async def handle_request(self, params, service, source_domain): + if service != "device-agent.GUI": + raise ValueError("unknown service name: {}".format(service)) + source = params["source"] + device_name = params["device_name"] + argument = params["argument"] + targets = params["targets"] + default_target = params["default_target"] + + entries_info = {} + for domain_name, icon in params["icons"].items(): + entries_info[domain_name] = {"icon": icon} + + target = await confirm_attachment( + entries_info, + source, + device_name, + argument, + targets, + default_target or None, + ) + + if target: + return f"allow:{target}" + return "deny" + + +parser = argparse.ArgumentParser() + +parser.add_argument( + "-s", + "--socket-path", + metavar="DIR", + type=str, + default=DEVICE_AGENT_SOCKET_PATH, + help="path to socket", +) + + +def main(): + args = parser.parse_args() + + gbulb.install() + agent = DeviceAgent(args.socket_path) + + asyncio.run(agent.run()) + + +if __name__ == "__main__": + main() diff --git a/rpm_spec/qubes-desktop-linux-manager.spec.in b/rpm_spec/qubes-desktop-linux-manager.spec.in index 8ab30de2..885e4f83 100644 --- a/rpm_spec/qubes-desktop-linux-manager.spec.in +++ b/rpm_spec/qubes-desktop-linux-manager.spec.in @@ -92,11 +92,14 @@ fi gtk-update-icon-cache %{_datadir}/icons/Adwaita &>/dev/null || : %files +/usr/bin/qubes-device-agent %defattr(-,root,root,-) %dir %{python3_sitelib}/qui-*.egg-info %{python3_sitelib}/qui-*.egg-info/* +/usr/lib/qubes/qubes-device-agent-autostart + %dir %{python3_sitelib}/qui %dir %{python3_sitelib}/qui/__pycache__ %{python3_sitelib}/qui/__pycache__/* @@ -137,6 +140,13 @@ gtk-update-icon-cache %{_datadir}/icons/Adwaita &>/dev/null || : %{python3_sitelib}/qui/devices/device_widget.py %{python3_sitelib}/qui/qubes-devices-dark.css %{python3_sitelib}/qui/qubes-devices-light.css +%{python3_sitelib}/qui/devices/AttachConfirmationWindow.glade + +%dir %{python3_sitelib}/qui/tools +%dir %{python3_sitelib}/qui/tools/__pycache__ +%{python3_sitelib}/qui/tools/__pycache__/* +%{python3_sitelib}/qui/tools/__init__.py +%{python3_sitelib}/qui/tools/qubes_device_agent.py %dir %{python3_sitelib}/qui/tray/ %dir %{python3_sitelib}/qui/tray/__pycache__ @@ -202,6 +212,7 @@ gtk-update-icon-cache %{_datadir}/icons/Adwaita &>/dev/null || : %{_bindir}/qui-updates %{_bindir}/qui-clipboard %{_bindir}/qubes-update-gui +/etc/xdg/autostart/qubes-device-agent.desktop /etc/xdg/autostart/qui-domains.desktop /etc/xdg/autostart/qui-devices.desktop /etc/xdg/autostart/qui-clipboard.desktop diff --git a/setup.py b/setup.py index cee4703f..09104331 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,38 @@ def create_mo_files(self): def run(self): self.create_mo_files() + self.install_custom_scripts() super().run() + # create simple scripts that run much faster than "console entry points" + def install_custom_scripts(self): + bin = os.path.join(self.root, "usr/bin") + try: + os.makedirs(bin) + except: + pass + for file, pkg in get_console_scripts(): + path = os.path.join(bin, file) + with open(path, "w") as f: + f.write( +"""#!/usr/bin/python3 +from {} import main +import sys +if __name__ == '__main__': + sys.exit(main()) +""".format(pkg)) + + os.chmod(path, 0o755) + +# don't import: import * is unreliable and there is no need, since this is +# compile time and we have source files +def get_console_scripts(): + for filename in os.listdir('./qui/tools'): + basename, ext = os.path.splitext(os.path.basename(filename)) + if basename == '__init__' or ext != '.py': + continue + yield basename.replace('_', '-'), 'qui.tools.{}'.format(basename) + setuptools.setup( name='qui', @@ -43,9 +73,10 @@ def run(self): description='Qubes User Interface And Configuration Package', license='GPL2+', url='https://www.qubes-os.org/', - packages=["qui", "qui.updater", "qui.devices", "qui.tray", "qubes_config", - "qubes_config.global_config", "qubes_config.widgets", - "qubes_config.new_qube", 'qubes_config.policy_editor'], + packages=["qui", "qui.updater", "qui.devices", "qui.tools", "qui.tray", + "qubes_config", "qubes_config.global_config", + "qubes_config.widgets", "qubes_config.new_qube", + 'qubes_config.policy_editor'], entry_points={ 'gui_scripts': [ 'qui-domains = qui.tray.domains:main', @@ -69,7 +100,8 @@ def run(self): "styles/qubes-widgets-base.css", "eol.json", "qubes-devices-light.css", - "qubes-devices-dark.css" + "qubes-devices-dark.css", + "devices/AttachConfirmationWindow.glade" ], 'qubes_config': ["new_qube.glade", "global_config.glade",