# -*- coding: UTF-8 -*- #addonHandler.py #A part of NonVisual Desktop Access (NVDA) #Copyright (C) 2012-2016 Rui Batista, NV Access Limited, Noelia Ruiz Martínez, Joseph Lee #This file is covered by the GNU General Public License. #See the file COPYING for more details. import sys import os.path import gettext import glob import tempfile import cPickle import inspect import itertools import collections import pkgutil import shutil from cStringIO import StringIO import zipfile from configobj import ConfigObj, ConfigObjError from validate import Validator import config import globalVars import languageHandler from logHandler import log import winKernel MANIFEST_FILENAME = "manifest.ini" stateFilename="addonsState.pickle" BUNDLE_EXTENSION = "nvda-addon" BUNDLE_MIMETYPE = "application/x-nvda-addon" NVDA_ADDON_PROG_ID = "NVDA.Addon.1" ADDON_PENDINGINSTALL_SUFFIX=".pendingInstall" DELETEDIR_SUFFIX=".delete" state={} def loadState(): global state statePath=os.path.join(globalVars.appArgs.configPath,stateFilename) try: state = cPickle.load(file(statePath, "r")) except: # Defaults. state = { "pendingRemovesSet":set(), "pendingInstallsSet":set(), "disabledAddons":set(), "pendingDisableSet":set(), } def saveState(): statePath=os.path.join(globalVars.appArgs.configPath,stateFilename) try: cPickle.dump(state, file(statePath, "wb")) except: log.debugWarning("Error saving state", exc_info=True) def getRunningAddons(): """ Returns currently loaded addons. """ return (addon for addon in getAvailableAddons() if addon.isRunning) def completePendingAddonRemoves(): """Removes any addons that could not be removed on the last run of NVDA""" user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) pendingRemovesSet=state['pendingRemovesSet'] for addonName in list(pendingRemovesSet): addonPath=os.path.join(user_addons,addonName) if os.path.isdir(addonPath): addon=Addon(addonPath) try: addon.completeRemove() except RuntimeError: log.exception("Failed to remove %s add-on"%addonName) continue pendingRemovesSet.discard(addonName) def completePendingAddonInstalls(): user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) pendingInstallsSet=state['pendingInstallsSet'] for addonName in pendingInstallsSet: newPath=os.path.join(user_addons,addonName) oldPath=newPath+ADDON_PENDINGINSTALL_SUFFIX try: os.rename(oldPath,newPath) except: log.error("Failed to complete addon installation for %s"%addonName,exc_info=True) pendingInstallsSet.clear() def removeFailedDeletions(): user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) for p in os.listdir(user_addons): if p.endswith(DELETEDIR_SUFFIX): path=os.path.join(user_addons,p) shutil.rmtree(path,ignore_errors=True) if os.path.exists(path): log.error("Failed to delete path %s, try removing manually"%path) _disabledAddons = set() def disableAddonsIfAny(): """Disables add-ons if told to do so by the user from add-ons manager""" global _disabledAddons if "disabledAddons" not in state: state["disabledAddons"] = set() if "pendingDisableSet" not in state: state["pendingDisableSet"] = set() if "pendingEnableSet" not in state: state["pendingEnableSet"] = set() # Pull in and enable add-ons that should be disabled and enabled, respectively. state["disabledAddons"] |= state["pendingDisableSet"] state["disabledAddons"] -= state["pendingEnableSet"] _disabledAddons = state["disabledAddons"] state["pendingDisableSet"].clear() state["pendingEnableSet"].clear() def initialize(): """ Initializes the add-ons subsystem. """ loadState() removeFailedDeletions() completePendingAddonRemoves() completePendingAddonInstalls() # #3090: Are there add-ons that are supposed to not run for this session? disableAddonsIfAny() saveState() getAvailableAddons(refresh=True) def terminate(): """ Terminates the add-ons subsystem. """ pass def _getDefaultAddonPaths(): """ Returns paths where addons can be found. For now, only " % self._path def createAddonBundleFromPath(path, destDir=None): """ Creates a bundle from a directory that contains a a addon manifest file.""" basedir = os.path.abspath(path) # If caller did not provide a destination directory name # Put the bundle at the same level of the addon's top directory, # That is, basedir/.. if destDir is None: destDir = os.path.dirname(basedir) manifest_path = os.path.join(basedir, MANIFEST_FILENAME) if not os.path.isfile(manifest_path): raise AddonError("Can't find %s manifest file." % manifest_path) with open(manifest_path) as f: manifest = AddonManifest(f) if manifest.errors is not None: _report_manifest_errors(manifest) raise AddonError("Manifest file as errors.") bundleFilename = "%s-%s.%s" % (manifest['name'], manifest['version'], BUNDLE_EXTENSION) bundleDestination = os.path.join(destDir, bundleFilename) with zipfile.ZipFile(bundleDestination, 'w') as z: # FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled. for dir, dirnames, filenames in os.walk(basedir): relativePath = os.path.relpath(dir, basedir) for filename in filenames: pathInBundle = os.path.join(relativePath, filename) absPath = os.path.join(dir, filename) z.write(absPath, pathInBundle) return AddonBundle(bundleDestination) def _report_manifest_errors(manifest): log.warning("Error loading manifest:\n%s", manifest.errors) class AddonManifest(ConfigObj): """ Add-on manifest file. It contains metadata about an NVDA add-on package. """ configspec = ConfigObj(StringIO( """ # NVDA Add-on Manifest configuration specification # Add-on unique name name = string() # short summary (label) of the add-on to show to users. summary = string() # Long description with further information and instructions description = string(default=None) # Name of the author or entity that created the add-on author = string() # Version of the add-on. Should preferably in some standard format such as x.y.z version = string() # URL for more information about the add-on. New versions and such. url= string(default=None) # Name of default documentation file for the add-on. docFileName = string(default=None) """)) def __init__(self, input, translatedInput=None): """ Constructs an L{AddonManifest} instance from manifest string data @param input: data to read the manifest informatinon @type input: a fie-like object. @param translatedInput: translated manifest input @type translatedInput: file-like object """ super(AddonManifest, self).__init__(input, configspec=self.configspec, encoding='utf-8', default_encoding='utf-8') self._errors = [] val = Validator() result = self.validate(val, copy=True, preserve_errors=True) if result != True: self._errors = result self._translatedConfig = None if translatedInput is not None: self._translatedConfig = ConfigObj(translatedInput, encoding='utf-8', default_encoding='utf-8') for k in ('summary','description'): val=self._translatedConfig.get(k) if val: self[k]=val @property def errors(self): return self._errors