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 @@
+
+
+
+
+
+
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",