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.
This commit is contained in:
Pawel Marczewski 2020-01-27 12:22:45 +01:00
parent 943f37b481
commit 3a6e77aa43
No known key found for this signature in database
GPG Key ID: DE42EE9B14F96465
10 changed files with 173 additions and 17 deletions

View File

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

View File

@ -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)/

View File

@ -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`.

View File

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

View File

@ -33,7 +33,7 @@ noextract=()
md5sums=(SKIP) md5sums=(SKIP)
build() { 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 # shellcheck disable=SC2154
(ln -s "$srcdir/../$source" "$srcdir/$source") (ln -s "$srcdir/../$source" "$srcdir/$source")
done done

View File

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

View File

@ -24,23 +24,14 @@
import sys import sys
from xdg.DesktopEntry import DesktopEntry 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 xdg.BaseDirectory
import os import os
QUBES_XDG_CONFIG_DROPINS = '/etc/qubes/autostart' 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): def entry_should_be_started(entry, environments):
""" """
@ -80,7 +71,10 @@ def process_autostart(environments):
entry_path = os.path.join(path, entry_name) entry_path = os.path.join(path, entry_name)
# files in $HOME have higher priority than dropins # files in $HOME have higher priority than dropins
if not path.startswith(xdg.BaseDirectory.xdg_config_home): 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: else:
entry = DesktopEntry(entry_path) entry = DesktopEntry(entry_path)
if entry_should_be_started(entry, environments): if entry_should_be_started(entry, environments):

67
qubesagent/test_xdg.py Normal file
View File

@ -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())

View File

@ -4,17 +4,78 @@ from gi.repository import Gio # pylint: disable=import-error
from gi.repository import GLib # pylint: disable=import-error from gi.repository import GLib # pylint: disable=import-error
import sys import sys
import os 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): def pid_callback(launcher, pid, pid_list):
pid_list.append(pid) pid_list.append(pid)
def dbus_name_change(loop, name, old_owner, new_owner): def dbus_name_change(loop, name, old_owner, new_owner):
if not new_owner: if not new_owner:
loop.quit() loop.quit()
def launch(desktop, *files, **kwargs):
def launch(filename, *files, **kwargs):
wait = kwargs.pop('wait', True) wait = kwargs.pop('wait', True)
launcher = Gio.DesktopAppInfo.new_from_filename(desktop) launcher = make_launcher(filename)
try: try:
import dbus import dbus
from dbus.mainloop.glib import DBusGMainLoop from dbus.mainloop.glib import DBusGMainLoop

View File

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