Browse Source

Add /etc/qubes/applications override, use it for gnome-terminal

Used by qubes.StartApp so that we can override distribution-provided
.desktop files. The mechanism is introduced to run gnome-terminal
with --wait option, so that it's compatible with DispVMs.

Fixes QubesOS/qubes-issues#2581.
Pawel Marczewski 4 years ago
parent
commit
3a6e77aa43

+ 1 - 0
Makefile

@@ -186,6 +186,7 @@ install-doc:
 
 install-common: install-doc
 	$(MAKE) -C autostart-dropins install
+	$(MAKE) -C applications-dropins install
 	install -m 0644 -D misc/fstab $(DESTDIR)/etc/fstab
 
 	# force /usr/bin before /bin to have /usr/bin/python instead of /bin/python

+ 6 - 0
applications-dropins/Makefile

@@ -0,0 +1,6 @@
+
+DROPINS_DIR = /etc/qubes/applications
+
+install:
+	for f in *.desktop; do install -m 0644 -D $$f $(DESTDIR)$(DROPINS_DIR)/$$f.d/30_qubes.conf; done
+	install -m 0644 README.txt $(DESTDIR)$(DROPINS_DIR)/

+ 20 - 0
applications-dropins/README.txt

@@ -0,0 +1,20 @@
+This directory (/etc/qubes/applications) is used to override parts of files in
+/usr/share/applications and other applications directories.
+
+For each desktop file there, you can create directory named after the file plus
+".d", then place files there. All such files will be read (in lexicographical
+order) and lines specified there will override respective entries in the
+original file.
+
+This can be used for example to override behaviour of a specific application in
+particular VM type.
+
+For example, you can extend `/usr/share/applications/firefox.desktop` by
+creating `/etc/qubes/applications/firefox.desktop.d/50_user.conf` with:
+```
+[Desktop Entry]
+Exec=firefox --private-window http://example.com %u
+```
+
+This would mean that `Exec` key would be read as your command line, regardless
+of original entry in `/usr/share/applications/firefox.desktop`.

+ 2 - 0
applications-dropins/org.gnome.Terminal.desktop

@@ -0,0 +1,2 @@
+[Desktop Entry]
+Exec=qubes-run-gnome-terminal

+ 1 - 1
archlinux/PKGBUILD

@@ -33,7 +33,7 @@ noextract=()
 md5sums=(SKIP)
 
 build() {
-    for source in autostart-dropins qubes-rpc qrexec misc Makefile vm-init.d vm-systemd network init version doc setup.py qubesagent post-install.d; do
+    for source in autostart-dropins applications-dropins qubes-rpc qrexec misc Makefile vm-init.d vm-systemd network init version doc setup.py qubesagent post-install.d; do
         # shellcheck disable=SC2154
         (ln -s "$srcdir/../$source" "$srcdir/$source")
     done

+ 1 - 0
debian/qubes-core-agent.install

@@ -36,6 +36,7 @@ etc/qubes-rpc/qubes.WaitForSession
 etc/qubes-rpc/qubes.GetDate
 etc/qubes-suspend-module-blacklist
 etc/qubes/autostart/*
+etc/qubes/applications/*
 etc/qubes/post-install.d/README
 etc/qubes/post-install.d/*.sh
 etc/qubes/rpc-config/qubes.OpenInVM

+ 7 - 13
misc/qubes-session-autostart

@@ -24,22 +24,13 @@
 import sys
 
 from xdg.DesktopEntry import DesktopEntry
-from qubesagent.xdg import launch
+from qubesagent.xdg import launch, find_dropins, \
+    load_desktop_entry_with_dropins
 import xdg.BaseDirectory
 import os
 
-QUBES_XDG_CONFIG_DROPINS = '/etc/qubes/autostart'
-
-def open_desktop_entry_and_dropins(filename):
-    desktop_entry = DesktopEntry(filename)
-    dropins_dir = os.path.join(QUBES_XDG_CONFIG_DROPINS,
-                               os.path.basename(filename) + '.d')
-    if os.path.isdir(dropins_dir):
-        for dropin in sorted(os.listdir(dropins_dir)):
-            dropin_content = DesktopEntry(os.path.join(dropins_dir, dropin))
-            desktop_entry.content.update(dropin_content.content)
 
-    return desktop_entry
+QUBES_XDG_CONFIG_DROPINS = '/etc/qubes/autostart'
 
 
 def entry_should_be_started(entry, environments):
@@ -80,7 +71,10 @@ def process_autostart(environments):
                 entry_path = os.path.join(path, entry_name)
                 # files in $HOME have higher priority than dropins
                 if not path.startswith(xdg.BaseDirectory.xdg_config_home):
-                    entry = open_desktop_entry_and_dropins(entry_path)
+                    dropins = find_dropins(
+                        entry_path, QUBES_XDG_CONFIG_DROPINS)
+                    entry = load_desktop_entry_with_dropins(
+                        entry_path, dropins)
                 else:
                     entry = DesktopEntry(entry_path)
                 if entry_should_be_started(entry, environments):

+ 67 - 0
qubesagent/test_xdg.py

@@ -0,0 +1,67 @@
+from unittest import TestCase
+import tempfile
+import shutil
+import os
+
+from qubesagent.xdg import find_dropins, load_desktop_entry_with_dropins, \
+    ini_to_string
+
+
+class TestXdg(TestCase):
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tempdir)
+
+    def test_00_load_desktop_entry(self):
+        filename = os.path.join(self.tempdir, 'firefox.desktop')
+        dropins_dir = os.path.join(self.tempdir, 'dropins')
+        dropin_filename = os.path.join(
+            self.tempdir, 'dropins', 'firefox.desktop.d', '030_qubes.conf')
+
+        with open(filename, 'w') as f:
+            f.write('''\
+[Desktop Entry]
+Name=Firefox
+Exec=firefox %u
+''')
+
+        os.makedirs(os.path.dirname(dropin_filename))
+        with open(dropin_filename, 'w') as f:
+            f.write('''\
+[Desktop Entry]
+Exec=my-firefox %u
+
+[Other Group]
+X-Key=yes
+''')
+
+        dropins = find_dropins(filename, dropins_dir)
+        self.assertListEqual(
+            dropins,
+            [dropin_filename])
+
+        desktop_entry = load_desktop_entry_with_dropins(filename, dropins)
+        self.assertEqual(desktop_entry.content['Desktop Entry']['Name'],
+                         'Firefox')
+        self.assertEqual(desktop_entry.content['Desktop Entry']['Exec'],
+                         'my-firefox %u')
+        self.assertEqual(desktop_entry.content['Other Group']['X-Key'],
+                         'yes')
+
+    def test_01_init_to_string(self):
+        filename = os.path.join(self.tempdir, 'firefox.desktop')
+        content = '''\
+[Desktop Entry]
+Name=Firefox
+Exec=firefox %u
+
+[Other Group]
+X-Key=yes
+'''
+
+        with open(filename, 'w') as f:
+            f.write(content)
+
+        desktop_entry = load_desktop_entry_with_dropins(filename, [])
+        output = ini_to_string(desktop_entry)
+        self.assertEqual(output.rstrip(), content.rstrip())

+ 63 - 2
qubesagent/xdg.py

@@ -4,17 +4,78 @@ from gi.repository import Gio  # pylint: disable=import-error
 from gi.repository import GLib  # pylint: disable=import-error
 import sys
 import os
+import io
+from xdg.DesktopEntry import DesktopEntry
+
+
+DROPINS_DIR = '/etc/qubes/applications'
+
+
+def find_dropins(filename, dropins_dir):
+    result = []
+    app_dropins_dir = os.path.join(
+        dropins_dir,
+        os.path.basename(filename) + '.d')
+    if os.path.isdir(app_dropins_dir):
+        for dropin in sorted(os.listdir(app_dropins_dir)):
+            result.append(
+                os.path.join(app_dropins_dir, dropin))
+    return result
+
+
+def load_desktop_entry_with_dropins(filename, dropins):
+    desktop_entry = DesktopEntry(filename)
+    for dropin in dropins:
+        dropin_entry = DesktopEntry(dropin)
+        for group_name, group in dropin_entry.content.items():
+            desktop_entry.content.setdefault(group_name, {}).update(group)
+    return desktop_entry
+
+
+def make_launcher(filename, dropins_dir=DROPINS_DIR):
+    dropins = find_dropins(filename, dropins_dir)
+    if not dropins:
+        return Gio.DesktopAppInfo.new_from_filename(filename)
+
+    desktop_entry = load_desktop_entry_with_dropins(filename, dropins)
+
+    data = GLib.Bytes(ini_to_string(desktop_entry).encode('utf-8'))
+    keyfile = GLib.KeyFile()
+    keyfile.load_from_bytes(data, 0)
+    return Gio.DesktopAppInfo.new_from_keyfile(keyfile)
+
+
+def ini_to_string(ini):
+    # See IniFile.write() in xdg package.
+
+    output = io.StringIO()
+    if ini.defaultGroup:
+        output.write("[%s]\n" % ini.defaultGroup)
+        for (key, value) in ini.content[ini.defaultGroup].items():
+            output.write("%s=%s\n" % (key, value))
+        output.write("\n")
+    for (name, group) in ini.content.items():
+        if name != ini.defaultGroup:
+            output.write("[%s]\n" % name)
+            for (key, value) in group.items():
+                output.write("%s=%s\n" % (key, value))
+            output.write("\n")
+
+    return output.getvalue()
+
 
 def pid_callback(launcher, pid, pid_list):
     pid_list.append(pid)
 
+
 def dbus_name_change(loop, name, old_owner, new_owner):
     if not new_owner:
         loop.quit()
 
-def launch(desktop, *files, **kwargs):
+
+def launch(filename, *files, **kwargs):
     wait = kwargs.pop('wait', True)
-    launcher = Gio.DesktopAppInfo.new_from_filename(desktop)
+    launcher = make_launcher(filename)
     try:
         import dbus
         from dbus.mainloop.glib import DBusGMainLoop

+ 5 - 1
rpm_spec/core-agent.spec.in

@@ -577,10 +577,13 @@ rm -f %{name}-%{version}
 %config(noreplace) /etc/qubes/rpc-config/qubes.InstallUpdatesGUI
 %config(noreplace) /etc/qubes/rpc-config/qubes.VMShell+WaitForSession
 %config(noreplace) /etc/qubes/rpc-config/qubes.VMExecGUI
-%dir /etc/qubes/autostart
 %config(noreplace) /etc/default/grub.qubes
+%dir /etc/qubes/autostart
 /etc/qubes/autostart/README.txt
 %config /etc/qubes/autostart/*.desktop.d/30_qubes.conf
+%dir /etc/qubes/applications
+/etc/qubes/applications/README.txt
+%config /etc/qubes/applications/*.desktop.d/30_qubes.conf
 %dir /etc/qubes/suspend-pre.d
 /etc/qubes/suspend-pre.d/README
 %dir /etc/qubes/suspend-post.d
@@ -675,6 +678,7 @@ rm -f %{name}-%{version}
 %{python3_sitelib}/qubesagent/vmexec.py*
 %{python3_sitelib}/qubesagent/test_vmexec.py*
 %{python3_sitelib}/qubesagent/xdg.py*
+%{python3_sitelib}/qubesagent/test_xdg.py*
 
 /usr/share/qubes/mime-override/globs
 /usr/share/qubes/qubes-master-key.asc