From e1cea1f50b96ef4bc44ab7bf2de5aff94459cf51 Mon Sep 17 00:00:00 2001 From: Marek Marczykowski Date: Fri, 20 May 2011 16:36:48 +0200 Subject: [PATCH] dom0: tool for sync desktop file templates (#45) --- dom0/qvm-core/qubes.py | 1 + dom0/qvm-tools/qvm-sync-appmenus | 192 +++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100755 dom0/qvm-tools/qvm-sync-appmenus diff --git a/dom0/qvm-core/qubes.py b/dom0/qvm-core/qubes.py index 885988a8..67355b9f 100755 --- a/dom0/qvm-core/qubes.py +++ b/dom0/qvm-core/qubes.py @@ -47,6 +47,7 @@ if not dry_run: qubes_guid_path = "/usr/bin/qubes_guid" qrexec_daemon_path = "/usr/lib/qubes/qrexec_daemon" +qrexec_client_path = "/usr/lib/qubes/qrexec_client" qubes_base_dir = "/var/lib/qubes" diff --git a/dom0/qvm-tools/qvm-sync-appmenus b/dom0/qvm-tools/qvm-sync-appmenus new file mode 100755 index 00000000..c0f46fd4 --- /dev/null +++ b/dom0/qvm-tools/qvm-sync-appmenus @@ -0,0 +1,192 @@ +#!/usr/bin/python +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2011 Marek Marczykowski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import subprocess +import re +import os +import sys +import fnmatch +from optparse import OptionParser +from qubes.qubes import QubesVmCollection,QubesException +from qubes.qubes import qrexec_client_path,default_appmenus_templates_subdir + +#TODO: qrexec_client_path + +# fields required to be present (and verified) in retrieved desktop file +required_fields = [ "Name", "Exec" ] + +# regexps for sanitization of retrieved values +std_re = re.compile(r"^[/a-zA-Z0-9.,&() -]*$") +fields_regexp = { + "Name": std_re, + "GenericName": std_re, + "Comment": std_re, + "Categories": re.compile(r"^[a-zA-Z0-9/.; -]*$"), + "Exec": re.compile(r"^[a-zA-Z0-9%>/:.= -]*$"), +} + +def get_appmenus(xid): + p = subprocess.Popen ([qrexec_client_path, '-d', str(xid), + 'user:grep -H = /usr/share/applications/*.desktop'], stdout=subprocess.PIPE) + untrusted_appmenulist = p.communicate()[0].split('\n') + if p.returncode != 0: + raise QubesException("Error getting application list") + + + row_no = 0 + appmenus = {} + line_rx = re.compile(r"([a-zA-Z0-9-.]+.desktop):([a-zA-Z0-9-]+(?:\[[a-zA-Z@_]+\])?)=(.*)") + ignore_rx = re.compile(r".*([a-zA-Z0-9-.]+.desktop):(#.*|\s+)$") + for untrusted_line in untrusted_appmenulist: + # Ignore blank lines and comments + if len(untrusted_line) == 0 or ignore_rx.match(untrusted_line): + continue + # use search instead of match to skip file path + untrusted_m = line_rx.search(untrusted_line) + if untrusted_m: + untrusted_key = untrusted_m.group(2) + untrusted_value = untrusted_m.group(3) + if fields_regexp.has_key(untrusted_key): + if fields_regexp[untrusted_key].match(untrusted_value): + # now values are sanitized + key = untrusted_key + value = untrusted_value + filename = untrusted_m.group(1) + + if not appmenus.has_key(filename): + appmenus[filename] = {} + + appmenus[filename][key]=value + else: + print "Warning: ignoring key %s: %s" % (untrusted_key, untrusted_value) + # else: ignore this key + else: + print "Warning: ignoring line: %s" % (untrusted_line); + + return appmenus + + +def create_template(path, values): + + # check if all required fields are present + for key in required_fields: + if not values.has_key(key): + print "Warning: not creating/updating '%s' because of missing '%s' key" % (path, key) + return + + desktop_file = open(path, "w") + desktop_file.write("[Desktop Entry]\n") + desktop_file.write("Version=1.0\n") + desktop_file.write("Type=Application\n") + desktop_file.write("Terminal=false\n") + desktop_file.write("X-Qubes-VmName=%VMNAME%\n") + desktop_file.write("Icon=%VMDIR%/icon.png\n") + for key in ["Name", "GenericName" ]: + if values.has_key(key): + desktop_file.write("{0}=%VMNAME%: {1}\n".format(key, values[key])) + + for key in [ "Comment", "Categories" ]: + if values.has_key(key): + desktop_file.write("{0}={1}\n".format(key, values[key])) + + desktop_file.write("Exec=qvm-run -q --tray -a %VMNAME% '{0}'\n".format(values['Exec'])) + desktop_file.close() + + +def main(): + + + usage = "usage: %prog [options] \n"\ + "Updates desktop file templates for given StandaloneVM or TemplateVM" + + parser = OptionParser (usage) + parser.add_option ("-v", "--verbose", action="store_true", dest="verbose", default=False) + + (options, args) = parser.parse_args () + if (len (args) != 1): + parser.error ("You must specify at least the VM name!") + + vmname=args[0] + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + vm = qvm_collection.get_vm_by_name(vmname) + + if vm is None: + print "ERROR: A VM with the name '{0}' does not exist in the system.".format(vmname) + exit(1) + + if not vm.is_updateable(): + print "ERROR: To sync appmenus for non-updateable VM, do it on template instead" + exit(1) + + if not vm.is_running(): + print "ERROR: Appmenus can be retrieved only from running VM - start it first" + exit(1) + + # Get appmenus from VM + xid = vm.get_xid() + assert xid > 0 + + new_appmenus = get_appmenus(xid) + + if len(new_appmenus) == 0: + print "ERROR: No appmenus received, terminating" + exit(1) + + # Contstruct path to work also for StandaloneVM + appmenus_templates = vm.dir_path + '/' + default_appmenus_templates_subdir + assert os.path.exists(appmenus_templates) + + # Create new/update existing templates + if options.verbose: + print "--> Got {0} appmenus, storing to disk".format(str(len(new_appmenus))) + for appmenu_file in new_appmenus.keys(): + if options.verbose: + if os.path.exists(appmenus_templates + '/' + appmenu_file): + print "---> Updating {0}".format(appmenu_file) + else: + print "---> Creating {0}".format(appmenu_file) + create_template(appmenus_templates + '/' + appmenu_file, new_appmenus[appmenu_file]) + + # Delete appmenus of remove applications + if options.verbose: + print "--> Cleaning old files" + for appmenu_file in os.listdir(appmenus_templates): + if not fnmatch.fnmatch(appmenu_file, '*.desktop'): + continue + + if not new_appmenus.has_key(appmenu_file): + if options.verbose: + print "---> Removing {0}".format(appmenu_file) + os.unlink(appmenus_templates + '/' + appmenu_file) + +main() + + + + + +