From 380dedfd16e846c12598326e2be366b3d389bad6 Mon Sep 17 00:00:00 2001 From: Jayant-Kernel Date: Mon, 16 Mar 2026 02:52:06 +0530 Subject: [PATCH 1/2] fix menu file leftover on vm remove --- qubesappmenus/__init__.py | 25 +++++++++++++++++ qubesappmenus/tests.py | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/qubesappmenus/__init__.py b/qubesappmenus/__init__.py index 15427a6..c163a62 100644 --- a/qubesappmenus/__init__.py +++ b/qubesappmenus/__init__.py @@ -501,11 +501,36 @@ def appmenus_remove(self, vm, refresh_cache=True): refresh_cache) shutil.rmtree(appmenus_dir) + self._remove_menu_files(vm) + if refresh_cache: if 'KDE_SESSION_UID' in os.environ: subprocess.call(['kbuildsycoca' + os.environ.get('KDE_SESSION_VERSION', '4')]) + @staticmethod + def _remove_menu_files(vm): + """Remove .menu files for a VM from ~/.config/menus/applications-merged/ + + xdg-desktop-menu uninstall does not always clean these up (errors + are suppressed, partial installs can leave entries behind). + """ + menus_dir = os.path.join(xdg.BaseDirectory.xdg_config_home, + 'menus', 'applications-merged') + if not os.path.isdir(menus_dir): + return + vm_name = str(vm) + escaped = vm_name_escape(vm_name) + for prefix in ('qubes-vm-directory', 'qubes-dispvm-directory'): + with contextlib.suppress(FileNotFoundError): + os.unlink(os.path.join(menus_dir, + 'user-' + prefix + escaped + '.menu')) + # also try old format (before VM name escaping was introduced) + with contextlib.suppress(FileNotFoundError): + os.unlink(os.path.join(menus_dir, + 'user-' + prefix + '-' + + vm_name + '.menu')) + def appicons_create(self, vm, srcdirs=(), force=False): """Create/update applications icons""" if not srcdirs: diff --git a/qubesappmenus/tests.py b/qubesappmenus/tests.py index 0430dd0..d2e05fc 100644 --- a/qubesappmenus/tests.py +++ b/qubesappmenus/tests.py @@ -24,6 +24,7 @@ import io import os +import shutil import tempfile import sys @@ -744,6 +745,64 @@ def test_132_get_available_does_not_require_unstable_flag( self.assertEqual(stdout.getvalue(), 'xterm.desktop - XTerm\n') + @unittest.mock.patch('subprocess.check_call') + def test_140_remove_cleans_menu_files(self, mock_subprocess): + 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-app', + klass='AppVM', + template=tpl, + virt_mode='pvh', + updateable=False, + provides_network=False, + label=self.app.labels[1]) + self.ext.appmenus_init(appvm) + self.ext.appmenus_create(appvm, refresh_cache=False) + + config_dir = tempfile.mkdtemp() + try: + menus_dir = os.path.join(config_dir, 'menus', 'applications-merged') + os.makedirs(menus_dir) + escaped = qubesappmenus.vm_name_escape(appvm.name) + # current-format .menu file + menu_file = os.path.join(menus_dir, + 'user-qubes-vm-directory' + escaped + '.menu') + # old-format .menu file (pre-escaping) + old_menu_file = os.path.join(menus_dir, + 'user-qubes-vm-directory-' + appvm.name + '.menu') + for path in (menu_file, old_menu_file): + with open(path, 'w', encoding='utf-8') as f: + f.write('\n') + + with unittest.mock.patch('xdg.BaseDirectory.xdg_config_home', + config_dir): + self.ext.appmenus_remove(appvm, refresh_cache=False) + + self.assertPathNotExists(menu_file) + self.assertPathNotExists(old_menu_file) + finally: + shutil.rmtree(config_dir) + + def test_141_remove_menu_files_missing_dir(self): + config_dir = tempfile.mkdtemp() + try: + menus_dir = os.path.join(config_dir, 'menus', 'applications-merged') + with unittest.mock.patch('xdg.BaseDirectory.xdg_config_home', + config_dir), \ + unittest.mock.patch('os.unlink') as mock_unlink: + # Missing applications-merged directory should be ignored. + qubesappmenus.Appmenus._remove_menu_files('test-vm') + mock_unlink.assert_not_called() + self.assertFalse(os.path.exists(menus_dir)) + finally: + shutil.rmtree(config_dir) + + def list_tests(): return (TC_00_Appmenus,) From e39730ee832ddef78fdd04b0cf59b1eca030111b Mon Sep 17 00:00:00 2001 From: Jayant-Kernel Date: Sat, 28 Mar 2026 01:50:40 +0530 Subject: [PATCH 2/2] tests: assert deprecated unstable flag is ignored with warning --- qubesappmenus/tests.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qubesappmenus/tests.py b/qubesappmenus/tests.py index d2e05fc..7e793c8 100644 --- a/qubesappmenus/tests.py +++ b/qubesappmenus/tests.py @@ -744,6 +744,27 @@ def test_132_get_available_does_not_require_unstable_flag( appmenus_cls.return_value.get_available.assert_called_once_with(vm) self.assertEqual(stdout.getvalue(), 'xterm.desktop - XTerm\n') + @unittest.mock.patch('qubesappmenus.Appmenus') + def test_133_unstable_flag_is_ignored(self, appmenus_cls): + vm = TestVM('test-inst-vm', klass='AppVM', + label=self.app.labels[1]) + self.app.domains[vm.name] = vm + appmenus_cls.return_value.get_available.return_value = [ + ('xterm.desktop', 'XTerm')] + + with unittest.mock.patch('sys.stdout', new_callable=io.StringIO) \ + as stdout, \ + unittest.mock.patch('sys.stderr', new_callable=io.StringIO) \ + as stderr: + qubesappmenus.main( + ['--force-root', '--get-available', + '--i-understand-format-is-unstable', vm.name], + app=self.app) + + appmenus_cls.return_value.get_available.assert_called_once_with(vm) + self.assertEqual(stdout.getvalue(), 'xterm.desktop - XTerm\n') + self.assertIn('deprecated', stderr.getvalue()) + @unittest.mock.patch('subprocess.check_call') def test_140_remove_cleans_menu_files(self, mock_subprocess):