Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions qubesappmenus/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,99 @@ def test_007_created_dispvm(self):
self.assertIn(b'X-Qubes-NonDispvmExec=', content)
self.assertNotIn(b'X-Qubes-DispvmExec=', content)

def test_008_appmenus_update_when_template_for_dispvms_enabled(self):
"""Dispvm menu entries appear after template_for_dispvms is set True.

Regression test for QubesOS/qubes-issues#9194: setting
template_for_dispvms on a VM must regenerate menus so the
"Disposable:" submenu shows up.
"""
tpl = TestVM('test-inst-tpl',
klass='TemplateVM',
virt_mode='pvh',
updateable=True,
provides_network=False,
label=self.app.labels[1])
self.ext.appmenus_init(tpl)
appvm = TestVM('test-inst-dvm',
klass='AppVM',
template=tpl,
virt_mode='pvh',
updateable=False,
provides_network=False,
template_for_dispvms=False,
label=self.app.labels[1])
self.ext.appmenus_init(appvm)
with open(os.path.join(self.ext.templates_dirs(tpl)[0],
'evince.desktop'), 'wb') as f:
f.write(importlib.resources.files(
__package__).joinpath(
'test-data/evince.desktop.template').read_bytes())

# First create without dispvm — no "Disposable:" directory entry
self.ext.appmenus_create(appvm, refresh_cache=False)
appmenus_dir = self.ext.appmenus_dir(appvm)
dispvm_dir = os.path.join(appmenus_dir,
'qubes-dispvm-directory_test_dinst_ddvm.directory')
self.assertPathNotExists(dispvm_dir)

# Now simulate setting template_for_dispvms=True + appmenus-dispvm
appvm.template_for_dispvms = True
appvm.features['appmenus-dispvm'] = '1'
self.ext.appmenus_create(appvm, refresh_cache=False)

self.assertPathExists(dispvm_dir)
dispvm_evince = os.path.join(appmenus_dir,
'org.qubes-os.dispvm._test_dinst_ddvm.evince.desktop')
self.assertPathExists(dispvm_evince)

def test_009_appmenus_update_when_template_for_dispvms_disabled(self):
"""Dispvm menu entries are removed after template_for_dispvms is cleared.

Regression test for QubesOS/qubes-issues#9194: clearing
template_for_dispvms must regenerate menus so stale
"Disposable:" entries are removed.
"""
tpl = TestVM('test-inst-tpl2',
klass='TemplateVM',
virt_mode='pvh',
updateable=True,
provides_network=False,
label=self.app.labels[1])
self.ext.appmenus_init(tpl)
appvm = TestVM('test-inst-dvm2',
klass='AppVM',
template=tpl,
virt_mode='pvh',
updateable=False,
provides_network=False,
template_for_dispvms=True,
label=self.app.labels[1])
appvm.features['appmenus-dispvm'] = '1'
self.ext.appmenus_init(appvm)
with open(os.path.join(self.ext.templates_dirs(tpl)[0],
'evince.desktop'), 'wb') as f:
f.write(importlib.resources.files(
__package__).joinpath(
'test-data/evince.desktop.template').read_bytes())

# First create as dispvm template — "Disposable:" entries should exist
self.ext.appmenus_create(appvm, refresh_cache=False)
appmenus_dir = self.ext.appmenus_dir(appvm)
dispvm_dir = os.path.join(appmenus_dir,
'qubes-dispvm-directory_test_dinst_ddvm2.directory')
self.assertPathExists(dispvm_dir)

# Simulate clearing template_for_dispvms
appvm.template_for_dispvms = False
del appvm.features['appmenus-dispvm']
self.ext.appmenus_create(appvm, refresh_cache=False)

self.assertPathNotExists(dispvm_dir)
dispvm_evince = os.path.join(appmenus_dir,
'org.qubes-os.dispvm._test_dinst_ddvm2.evince.desktop')
self.assertPathNotExists(dispvm_evince)

def test_100_get_appmenus(self):
self.maxDiff = None
def _run(service, **kwargs):
Expand Down
8 changes: 8 additions & 0 deletions qubesappmenusext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ def provides_network_setter(self, vm, event, **kwargs):
self.vm_tasks[vm.name].append(
asyncio.ensure_future(self.update_appmenus(vm)))

@qubes.ext.handler('property-set:template_for_dispvms')
def template_for_dispvms_setter(self, vm, event, **kwargs):

@marmarek marmarek Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable-related menu entries are supposed to be created only if appmenus-dispvm feature is also set. There is already handling for setting this feature, so if one first sets template_for_dispvms property and then appmenus-dispvm feature, it will regenerate correctly. But if one first sets the feature, and only then the property, indeed regenerating is missing.

But, with the current version of this PR, when the user sets both the feature and the property (as qubes-vm-settings application does), regenerating will happen twice, in parallel, making QubesOS/qubes-issues#10908 worse. This should ensure regenerating is done only when expected entries change. This means:

  • here - only if appmenus-dispvm feature is already set
  • in the appmenus-dispvm feature handlers (both of them) - if template_for_dispvms property is True

if vm.app.vmm.offline_mode:
return
self.collect_done_tasks(vm)
self.vm_tasks[vm.name].append(
asyncio.ensure_future(self.update_appmenus(vm)))

@qubes.ext.handler('property-set:guivm')
def provides_network_setter(self, vm, event, name, newvalue, oldvalue=None):
if vm.app.vmm.offline_mode:
Expand Down