diff --git a/Makefile b/Makefile index 4f788cc..c166ba2 100644 --- a/Makefile +++ b/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 diff --git a/applications-dropins/Makefile b/applications-dropins/Makefile new file mode 100644 index 0000000..67cfe21 --- /dev/null +++ b/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)/ diff --git a/applications-dropins/README.txt b/applications-dropins/README.txt new file mode 100644 index 0000000..a84a43c --- /dev/null +++ b/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`. diff --git a/applications-dropins/org.gnome.Terminal.desktop b/applications-dropins/org.gnome.Terminal.desktop new file mode 100644 index 0000000..724807a --- /dev/null +++ b/applications-dropins/org.gnome.Terminal.desktop @@ -0,0 +1,2 @@ +[Desktop Entry] +Exec=qubes-run-gnome-terminal diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index c0828c0..ec77329 100644 --- a/archlinux/PKGBUILD +++ b/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 diff --git a/debian/qubes-core-agent.install b/debian/qubes-core-agent.install index 397c131..1a8879a 100644 --- a/debian/qubes-core-agent.install +++ b/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 diff --git a/misc/qubes-session-autostart b/misc/qubes-session-autostart index 1fa5765..b664159 100644 --- a/misc/qubes-session-autostart +++ b/misc/qubes-session-autostart @@ -24,23 +24,14 @@ 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 - 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): diff --git a/qubesagent/test_xdg.py b/qubesagent/test_xdg.py new file mode 100644 index 0000000..58a22a8 --- /dev/null +++ b/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()) diff --git a/qubesagent/xdg.py b/qubesagent/xdg.py index 3b9f08d..513ef03 100755 --- a/qubesagent/xdg.py +++ b/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 diff --git a/rpm_spec/core-agent.spec.in b/rpm_spec/core-agent.spec.in index 2b264d8..af6fda3 100644 --- a/rpm_spec/core-agent.spec.in +++ b/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