Skip to content

Commit

Permalink
appmenus: Call qvm-appmenus via qrexec service
Browse files Browse the repository at this point in the history
Add two new qrexec services: qubes.UpdateAppmenusFor and qubes.RemoveAppmenusFor.
When menu-relevant properties are changed on a VM, call qvm-appmenus via
those new services, instead of directly. This gives a bit nicer
interface when calling it in GUI VM that is not dom0 - which is a
feature added here at the same time.

Fixes QubesOS/qubes-issues#8528
  • Loading branch information
marmarek committed Sep 18, 2023
1 parent 8ed18dc commit f1964f9
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 21 deletions.
2 changes: 2 additions & 0 deletions Makefile
Expand Up @@ -19,6 +19,8 @@ install:

mkdir -p $(DESTDIR)/etc/qubes-rpc/policy
install -m 0755 qubesappmenus/qubes.SyncAppMenus $(DESTDIR)/etc/qubes-rpc/
install -m 0755 qubesappmenus/qubes.UpdateAppMenusFor $(DESTDIR)/etc/qubes-rpc/
install -m 0755 qubesappmenus/qubes.RemoveAppMenusFor $(DESTDIR)/etc/qubes-rpc/

$(MAKE) -C qubes-menus install

Expand Down
2 changes: 2 additions & 0 deletions debian/qubes-desktop-linux-common.install
Expand Up @@ -6,3 +6,5 @@ usr/lib/python3/dist-packages/qubesappmenus/*
usr/lib/python3/dist-packages/qubesdesktop-*.egg-info/*
usr/lib/python3/dist-packages/qubesappmenusext/*
etc/qubes-rpc/qubes.SyncAppMenus
etc/qubes-rpc/qubes.UpdateAppMenusFor
etc/qubes-rpc/qubes.RemoveAppMenusFor
15 changes: 15 additions & 0 deletions qubesappmenus/qubes.RemoveAppMenusFor
@@ -0,0 +1,15 @@
#!/bin/sh

if [ -z "$1" ]; then
echo "Missing argument (VM name)" >&2
exit 2
fi

if [ $(id -u) -eq 0 ]; then
user=$(getent group qubes|cut -d: -f4|cut -d, -f1)
if [ -n "$user" ]; then
exec /usr/sbin/runuser -u "$user" env DISPLAY=:0 /usr/bin/qvm-appmenus --remove --quiet -- "$1"
fi
else
/usr/bin/qvm-appmenus --remove --quiet -- "$1"
fi
15 changes: 15 additions & 0 deletions qubesappmenus/qubes.UpdateAppMenusFor
@@ -0,0 +1,15 @@
#!/bin/sh

if [ -z "$1" ]; then
echo "Missing argument (VM name)" >&2
exit 2
fi

if [ $(id -u) -eq 0 ]; then
user=$(getent group qubes|cut -d: -f4|cut -d, -f1)
if [ -n "$user" ]; then
exec /usr/sbin/runuser -u "$user" env DISPLAY=:0 /usr/bin/qvm-appmenus --update --force --quiet -- "$1"
fi
else
/usr/bin/qvm-appmenus --update --force --quiet -- "$1"
fi
79 changes: 58 additions & 21 deletions qubesappmenusext/__init__.py
Expand Up @@ -24,6 +24,7 @@
import asyncio

import qubes.ext
from qubes.utils import sanitize_stderr_for_log


class AppmenusExtension(qubes.ext.Extension):
Expand All @@ -32,13 +33,13 @@ def __init__(self, *args):
self.log = logging.getLogger('appmenus')

async def run_as_user(self, command):
'''
"""
Run given command (in subprocess.Popen acceptable format) as default
normal user
:param command: list for subprocess.Popen
:return: None
'''
"""
try:
qubes_group = grp.getgrnam('qubes')
user = qubes_group.gr_mem[0]
Expand All @@ -53,11 +54,55 @@ async def run_as_user(self, command):
if proc.returncode != 0:
self.log.warning('Command \'%s\' failed', ' '.join(command))

async def update_appmenus(self, vm):
guivm = vm.guivm
if not guivm:
vm.log.warning("VM for '%s' does not have GUI VM, not updating menu", vm.name)
return
if not guivm.is_running():
vm.log.warning("GUI VM for '%s' is not running, not updating menu", vm.name)
return
vm.log.info("Updating appmenus for '%s' in '%s'", vm.name, guivm.name)
if guivm.klass == 'AdminVM' or guivm.features.check_with_template("supported-rpc.qubes.UpdateAppMenusFor", None):
try:
await guivm.run_service_for_stdio("qubes.UpdateAppMenusFor+" + vm.name)
except subprocess.CalledProcessError as e:
vm.log.error("Failed to update appmenus for '%s' in '%s': %s",
vm.name, guivm.name, sanitize_stderr_for_log(e.stderr))
else:
# older desktop-linux-common
try:
await guivm.run_for_stdio("qvm-appmenus --update --quiet --force -- " + vm.name)
except subprocess.CalledProcessError as e:
vm.log.error("Failed to update appmenus for '%s' in '%s': %s",
vm.name, guivm.name, sanitize_stderr_for_log(e.stderr))

async def remove_appmenus(self, vm):
guivm = vm.guivm
if not guivm:
vm.log.warning("VM for '%s' does not have GUI VM, not removing menu", vm.name)
return
if not guivm.is_running():
vm.log.warning("GUI VM for '%s' is not running, not removing menu", vm.name)
return
vm.log.info("Removing appmenus for '%s' in '%s'", vm.name, guivm.name)
if guivm.klass == 'AdminVM' or guivm.features.check_with_template("supported-rpc.qubes.RemoveAppMenusFor", None):
try:
await guivm.run_service_for_stdio("qubes.RemoveAppMenusFor+" + vm.name)
except subprocess.CalledProcessError as e:
vm.log.error("Failed to remove appmenus for '%s' in '%s': %s",
vm.name, guivm.name, sanitize_stderr_for_log(e.stderr))
else:
# older desktop-linux-common
try:
await guivm.run_for_stdio("qvm-appmenus --remove --quiet -- " + vm.name)
except subprocess.CalledProcessError as e:
vm.log.error("Failed to remove appmenus for '%s' in '%s': %s",
vm.name, guivm.name, sanitize_stderr_for_log(e.stderr))

@qubes.ext.handler('domain-create-on-disk')
async def create_on_disk(self, vm, event):
await self.run_as_user(
['qvm-appmenus', '--quiet', '--init', '--create', vm.name])

await self.update_appmenus(vm)

@qubes.ext.handler('domain-clone-files')
async def clone_disk_files(self, vm, event, src):
Expand All @@ -67,47 +112,39 @@ async def clone_disk_files(self, vm, event, src):

@qubes.ext.handler('domain-remove-from-disk')
async def remove_from_disk(self, vm, event):
await self.run_as_user(
['qvm-appmenus', '--quiet', '--remove', vm.name])
await self.remove_appmenus(vm)

@qubes.ext.handler('property-set:label')
def label_setter(self, vm, event, **kwargs):
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--force', '--update', vm.name]))
asyncio.ensure_future(self.update_appmenus(vm))

@qubes.ext.handler('property-set:provides_network')
def provides_network_setter(self, vm, event, **kwargs):
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--force', '--update', vm.name]))
asyncio.ensure_future(self.update_appmenus(vm))

@qubes.ext.handler('domain-feature-delete:appmenus-dispvm')
def on_feature_del_appmenus_dispvm(self, vm, event, feature):
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--force', '--update', vm.name]))
asyncio.ensure_future(self.update_appmenus(vm))

@qubes.ext.handler('domain-feature-set:appmenus-dispvm')
def on_feature_set_appmenus_dispvm(self, vm, event, feature,
value, oldvalue=None):
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--force', '--update', vm.name]))
asyncio.ensure_future(self.update_appmenus(vm))

@qubes.ext.handler('domain-feature-set:menu-items')
def on_feature_set_appmenus_dispvm(self, vm, event, feature,
value, oldvalue=None):
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--force', '--update', vm.name]))
asyncio.ensure_future(self.update_appmenus(vm))

@qubes.ext.handler('domain-feature-delete:internal')
def on_feature_del_internal(self, vm, event, feature):
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--create', vm.name]))
asyncio.ensure_future(self.update_appmenus(vm))

@qubes.ext.handler('domain-feature-set:internal')
def on_feature_set_internal(self, vm, event, feature, value,
oldvalue=None):
if value:
asyncio.ensure_future(self.run_as_user(
['qvm-appmenus', '--quiet', '--remove', vm.name]))
asyncio.ensure_future(self.remove_appmenus(vm))

@qubes.ext.handler('template-postinstall')
def on_template_postinstall(self, vm, event):
Expand Down
2 changes: 2 additions & 0 deletions rpm_spec/desktop-linux-common.spec.in
Expand Up @@ -109,6 +109,8 @@ fi
%{python3_sitelib}/qubesappmenusext/__init__.py

/etc/qubes-rpc/qubes.SyncAppMenus
/etc/qubes-rpc/qubes.UpdateAppMenusFor
/etc/qubes-rpc/qubes.RemoveAppMenusFor
/usr/share/qubes/icons/*.png
/usr/bin/qvm-sync-appmenus
/usr/bin/qvm-appmenus
Expand Down

0 comments on commit f1964f9

Please sign in to comment.