No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

plugins.py 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. #-*- coding: utf-8 -*-
  2. import sys
  3. import os.path
  4. import importlib
  5. import copy
  6. import json
  7. from importlib.machinery import SourceFileLoader, SourcelessFileLoader
  8. import plugins
  9. from lodel import logger
  10. from lodel.settings.utils import SettingsError
  11. from .exceptions import *
  12. ## @package lodel.plugins Lodel2 plugins management
  13. #
  14. # Lodel2 plugins are stored in directories
  15. # A typicall lodel2 plugin directory structure looks like :
  16. # - {{__init__.py}}} containing informations like full_name, authors, licence etc.
  17. # - main.py containing hooks registration etc
  18. # - confspec.py containing a configuration specification dictionary named CONFSPEC
  19. ##@brief The package in which we will load plugins modules
  20. VIRTUAL_PACKAGE_NAME = 'lodel.plugins'
  21. ##@brief The temporary package to import python sources
  22. VIRTUAL_TEMP_PACKAGE_NAME = 'lodel.plugin_tmp'
  23. ##@brief Plugin init filename
  24. INIT_FILENAME = '__init__.py' # Loaded with settings
  25. PLUGIN_NAME_VARNAME = '__plugin_name__'
  26. PLUGIN_TYPE_VARNAME = '__plugin_type__'
  27. PLUGIN_VERSION_VARNAME = '__version__'
  28. CONFSPEC_FILENAME_VARNAME = '__confspec__'
  29. CONFSPEC_VARNAME = 'CONFSPEC'
  30. LOADER_FILENAME_VARNAME = '__loader__'
  31. PLUGIN_DEPS_VARNAME = '__plugin_deps__'
  32. ACTIVATE_METHOD_NAME = '_activate'
  33. ##@brief Discover stage cache filename
  34. DISCOVER_CACHE_FILENAME = '.plugin_discover_cache.json'
  35. ##@brief Default & failover value for plugins path list
  36. DEFAULT_PLUGINS_PATH_LIST = ['./plugins']
  37. MANDATORY_VARNAMES = [PLUGIN_NAME_VARNAME, LOADER_FILENAME_VARNAME,
  38. PLUGIN_VERSION_VARNAME]
  39. DEFAULT_PLUGIN_TYPE = 'extension'
  40. PLUGINS_TYPES = [DEFAULT_PLUGIN_TYPE, 'datasource', 'session_handler', 'ui']
  41. ##@brief Describe and handle version numbers
  42. class PluginVersion(object):
  43. PROPERTY_LIST = ['major', 'minor', 'revision' ]
  44. ##@brief Version constructor
  45. #@param *args : You can either give a str that will be splitted on . or you
  46. #can give a iterable containing 3 integer or 3 arguments representing
  47. #major, minor and revision version
  48. def __init__(self, *args):
  49. self.__version = [0 for _ in range(3) ]
  50. if len(args) == 1:
  51. arg = args[0]
  52. if isinstance(arg, str):
  53. spl = arg.split('.')
  54. invalid = False
  55. if len(spl) > 3:
  56. raise PluginError("The string '%s' is not a valid plugin \
  57. version number" % arg)
  58. else:
  59. try:
  60. if len(arg) >= 1:
  61. if len(arg) > 3:
  62. raise PluginError("Expected maximum 3 value to \
  63. create a plugin version number but found '%s' as argument" % arg)
  64. for i, v in enumerate(arg):
  65. self.__version[i] = arg[i]
  66. except TypeError:
  67. raise PluginError("Unable to convert argument into plugin \
  68. version number" % arg)
  69. elif len(args) > 3:
  70. raise PluginError("Expected between 1 and 3 positional arguments \
  71. but %d arguments found" % len(args))
  72. else:
  73. for i,v in enumerate(args):
  74. self.__version[i] = v
  75. @property
  76. def major(self):
  77. return self.__version[0]
  78. @property
  79. def minor(self):
  80. return self.__version[1]
  81. @property
  82. def revision(self):
  83. return self.__version[2]
  84. ##@brief Check and prepare comparisoon argument
  85. #@return A PluginVersion instance
  86. #@throw PluginError if invalid argument provided
  87. def __cmp_check(self, other):
  88. if not isinstance(other, PluginVersion):
  89. try:
  90. if len(other) <= 3 and len(other) > 0:
  91. return PluginVersion(other)
  92. except TypeError:
  93. raise PluginError("Cannot compare argument '%s' with \
  94. a PluginVerison instance" % other)
  95. return other
  96. ##@brief Generic comparison function
  97. #@param other PluginVersion or iterable
  98. #@param cmp_fun_name function : interger comparison function
  99. def __generic_cmp(self, other, cmp_fun_name):
  100. other = self.__cmp_check(other)
  101. try:
  102. cmpfun = getattr(int, cmp_fun_name)
  103. except AttributeError:
  104. raise LodelFatalError("Invalid comparison callback given \
  105. to generic PluginVersion comparison function : '%s'" % cmp_fun_name)
  106. for property_name in self.PROPERTY_LIST:
  107. if not cmpfun(getattr(self, pname), getattr(other, pname)):
  108. return False
  109. return True
  110. def __lt__(self, other):
  111. return self.__generic_cmp(other, '__lt__')
  112. def __le__(self, other):
  113. return self.__generic_cmp(other, '__le__')
  114. def __eq__(self, other):
  115. return self.__generic_cmp(other, '__eq__')
  116. def __ne__(self, other):
  117. return self.__generic_cmp(other, '__ne__')
  118. def __gt__(self, other):
  119. return self.__generic_cmp(other, '__gt__')
  120. def __ge__(self, other):
  121. return self.__generic_cmp(other, '__ge__')
  122. def __str__(self):
  123. return '%d.%d.%d' % tuple(self.__version)
  124. def __repr__(self):
  125. return {'major': self.major, 'minor': self.minor,
  126. 'revision': self.revision}
  127. ##@brief Stores plugin class registered
  128. __all_ptypes = list()
  129. ##@brief Plugin metaclass that allows to "catch" child class
  130. #declaration
  131. #
  132. #Automatic script registration on child class declaration
  133. class MetaPlugType(type):
  134. def __init__(self, name, bases, attrs):
  135. #Here we can store all child classes of Plugin
  136. super().__init__(name, bases, attrs)
  137. if len(bases) == 1 and bases[0] == object:
  138. print("Dropped : ", name, bases)
  139. return
  140. list_name= [cls._name__ for cls in plugin_types(self)]
  141. if self.name is in list_name:
  142. return
  143. else:
  144. plug_type_register(self)
  145. def plug_type_register(cls):
  146. __all__ptypes.append(cls)
  147. logger.info("New child class registered : %s" % cls.__name__)
  148. ##@brief Return a list of child Class Plugin
  149. @classmethod
  150. def plugin_types(cls):
  151. return cls.__all_ptypes
  152. ##@brief Handle plugins
  153. #
  154. # An instance represent a loaded plugin. Class methods allow to load/preload
  155. # plugins.
  156. #
  157. # Typicall Plugins load sequence is :
  158. # 1. Settings call start method to instanciate all plugins found in confs
  159. # 2. Settings fetch all confspecs
  160. # 3. the loader call load_all to register hooks etc
  161. class Plugin(object, metaclass=MetaPlugType):
  162. ##@brief Stores plugin directories paths
  163. _plugin_directories = None
  164. ##@brief Stores Plugin instances indexed by name
  165. _plugin_instances = dict()
  166. ##@brief Attribute used by load_all and load methods to detect circular
  167. #dependencies
  168. _load_called = []
  169. ##@brief Attribute that stores plugins list from discover cache file
  170. _plugin_list = None
  171. ##@brief Store dict representation of discover cache content
  172. _discover_cache = None
  173. ##@brief Plugin class constructor
  174. #
  175. # Called by setting in early stage of lodel2 boot sequence using classmethod
  176. # register
  177. #
  178. # @param plugin_name str : plugin name
  179. # @throw PluginError
  180. def __init__(self, plugin_name):
  181. self.started()
  182. self.name = plugin_name
  183. self.path = self.plugin_path(plugin_name)
  184. ##@brief Stores the plugin module
  185. self.module = None
  186. ##@breif Stores the plugin loader module
  187. self.__loader_module = None
  188. self.__confspecs = dict()
  189. self.loaded = False
  190. # Importing __init__.py infos in it
  191. plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
  192. plugin_name)
  193. init_source = os.path.join(self.path, INIT_FILENAME)
  194. try:
  195. loader = SourceFileLoader(plugin_module, init_source)
  196. self.module = loader.load_module()
  197. except (ImportError,FileNotFoundError) as e:
  198. raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid or the plugin do not exists" % plugin_name)
  199. # loading confspecs
  200. try:
  201. # Loading confspec directly from __init__.py
  202. self.__confspecs = getattr(self.module, CONFSPEC_VARNAME)
  203. except AttributeError:
  204. # Loading file in __confspec__ var in __init__.py
  205. try:
  206. module = self._import_from_init_var(CONFSPEC_FILENAME_VARNAME)
  207. except AttributeError:
  208. msg = "Malformed plugin {plugin} . No {varname} not {filevar} found in __init__.py"
  209. msg = msg.format(
  210. plugin = self.name,
  211. varname = CONFSPEC_VARNAME,
  212. filevar = CONFSPEC_FILENAME_VARNAME)
  213. raise PluginError(msg)
  214. except ImportError as e:
  215. msg = "Broken plugin {plugin} : {expt}"
  216. msg = msg.format(
  217. plugin = self.name,
  218. expt = str(e))
  219. raise PluginError(msg)
  220. try:
  221. # loading confpsecs from file
  222. self.__confspecs = getattr(module, CONFSPEC_VARNAME)
  223. except AttributeError:
  224. msg = "Broken plugin. {varname} not found in '{filename}'"
  225. msg = msg.format(
  226. varname = CONFSPEC_VARNAME,
  227. filename = confspec_filename)
  228. raise PluginError(msg)
  229. # loading plugin version
  230. try:
  231. #this try block should be useless. The existance of
  232. #PLUGIN_VERSION_VARNAME in init file is mandatory
  233. self.__version = getattr(self.module, PLUGIN_VERSION_VARNAME)
  234. except AttributeError:
  235. msg = "Error that should not append : no %s found in plugin \
  236. init file. Malformed plugin"
  237. msg %= PLUGIN_VERSION_VARNAME
  238. raise LodelFatalError(msg)
  239. # Load plugin type
  240. try:
  241. self.__type = getattr(self.module, PLUGIN_TYPE_VARNAME)
  242. except AttributeError:
  243. self.__type = DEFAULT_PLUGIN_TYPE
  244. self.__type = str(self.__type).lower()
  245. if self.__type not in PLUGINS_TYPES:
  246. raise PluginError("Unknown plugin type '%s'" % self.__type)
  247. # Load plugin name from init file (just for checking)
  248. try:
  249. #this try block should be useless. The existance of
  250. #PLUGIN_NAME_VARNAME in init file is mandatory
  251. pname = getattr(self.module, PLUGIN_NAME_VARNAME)
  252. except AttributeError:
  253. msg = "Error that should not append : no %s found in plugin \
  254. init file. Malformed plugin"
  255. msg %= PLUGIN_NAME_VARNAME
  256. raise LodelFatalError(msg)
  257. if pname != plugin_name:
  258. msg = "Plugin's discover cache inconsistency detected ! Cached \
  259. name differ from the one found in plugin's init file"
  260. raise PluginError(msg)
  261. ##@brief Try to import a file from a variable in __init__.py
  262. #@param varname str : The variable name
  263. #@return loaded module
  264. #@throw AttributeError if varname not found
  265. #@throw ImportError if the file fails to be imported
  266. #@throw PluginError if the filename was not valid
  267. def _import_from_init_var(self, varname):
  268. # Read varname
  269. try:
  270. filename = getattr(self.module, varname)
  271. except AttributeError:
  272. msg = "Malformed plugin {plugin}. No {varname} found in __init__.py"
  273. msg = msg.format(
  274. plugin = self.name,
  275. varname = LOADER_FILENAME_VARNAME)
  276. raise PluginError(msg)
  277. #Path are not allowed
  278. if filename != os.path.basename(filename):
  279. msg = "Invalid {varname} content : '{fname}' for plugin {name}"
  280. msg = msg.format(
  281. varname = varname,
  282. fname = filename,
  283. name = self.name)
  284. raise PluginError(msg)
  285. # importing the file in varname
  286. module_name = self.module.__name__+"."+varname
  287. filename = os.path.join(self.path, filename)
  288. loader = SourceFileLoader(module_name, filename)
  289. return loader.load_module()
  290. ##@brief Check dependencies of plugin
  291. #@return A list of plugin name to be loaded before
  292. def check_deps(self):
  293. try:
  294. res = getattr(self.module, PLUGIN_DEPS_VARNAME)
  295. except AttributeError:
  296. return list()
  297. result = list()
  298. errors = list()
  299. for plugin_name in res:
  300. try:
  301. result.append(self.get(plugin_name))
  302. except PluginError:
  303. errors.append(plugin_name)
  304. if len(errors) > 0:
  305. raise PluginError( "Bad dependencie for '%s' :"%self.name,
  306. ', '.join(errors))
  307. return result
  308. ##@brief Check if the plugin should be activated
  309. #
  310. #Try to fetch a function called @ref ACTIVATE_METHOD_NAME in __init__.py
  311. #of a plugin. If none found assert that the plugin can be loaded, else
  312. #the method is called. If it returns anything else that True, the plugin
  313. #is noted as not activable
  314. #
  315. # @note Maybe we have to exit everything if a plugin cannot be loaded...
  316. def activable(self):
  317. from lodel import logger
  318. try:
  319. test_fun = getattr(self.module, ACTIVATE_METHOD_NAME)
  320. except AttributeError:
  321. msg = "No %s method found for plugin %s. Assuming plugin is ready to be loaded"
  322. msg %= (ACTIVATE_METHOD_NAME, self.name)
  323. logger.debug(msg)
  324. test_fun = lambda:True
  325. return test_fun()
  326. ##@brief Load a plugin
  327. #
  328. #Loading a plugin means importing a file. The filename is defined in the
  329. #plugin's __init__.py file in a LOADER_FILENAME_VARNAME variable.
  330. #
  331. #The loading process has to take care of other things :
  332. #- loading dependencies (other plugins)
  333. #- check that the plugin can be activated using Plugin.activate() method
  334. #- avoid circular dependencies infinite loop
  335. def _load(self):
  336. if self.loaded:
  337. return
  338. from lodel import logger
  339. #Test that plugin "wants" to be activated
  340. activable = self.activable()
  341. if not(activable is True):
  342. msg = "Plugin %s is not activable : %s"
  343. msg %= (self.name, activable)
  344. raise PluginError(activable)
  345. #Circular dependencie detection
  346. if self.name in self._load_called:
  347. raise PluginError("Circular dependencie in Plugin detected. Abording")
  348. else:
  349. self._load_called.append(self.name)
  350. #Dependencie load
  351. for dependencie in self.check_deps():
  352. activable = dependencie.activable()
  353. if activable is True:
  354. dependencie._load()
  355. else:
  356. msg = "Plugin {plugin_name} not activable because it depends on plugin {dep_name} that is not activable : {reason}"
  357. msg = msg.format(
  358. plugin_name = self.name,
  359. dep_name = dependencie.name,
  360. reason = activable)
  361. #Loading the plugin
  362. try:
  363. self.__loader_module = self._import_from_init_var(LOADER_FILENAME_VARNAME)
  364. except PluginError as e:
  365. raise e
  366. except ImportError as e:
  367. msg = "Broken plugin {plugin} : {expt}"
  368. msg = msg.format(
  369. plugin = self.name,
  370. expt = str(e))
  371. raise PluginError(msg)
  372. logger.debug("Plugin '%s' loaded" % self.name)
  373. self.loaded = True
  374. def loader_module(self):
  375. if not self.loaded:
  376. raise RuntimeError("Plugin %s not loaded yet."%self.name)
  377. return self.__loader_module
  378. def __str__(self):
  379. return "<LodelPlugin '%s' version %s>" % (self.name, self.__version)
  380. ##@brief Call load method on every pre-loaded plugins
  381. #
  382. # Called by loader to trigger hooks registration.
  383. # This method have to avoid circular dependencies infinite loops. For this
  384. # purpose a class attribute _load_called exists.
  385. # @throw PluginError
  386. @classmethod
  387. def load_all(cls):
  388. errors = dict()
  389. cls._load_called = []
  390. for name, plugin in cls._plugin_instances.items():
  391. try:
  392. plugin._load()
  393. except PluginError as e:
  394. errors[name] = e
  395. if len(errors) > 0:
  396. msg = "Errors while loading plugins :"
  397. for name, e in errors.items():
  398. msg += "\n\t%20s : %s" % (name,e)
  399. msg += "\n"
  400. raise PluginError(msg)
  401. from lodel.plugin.hooks import LodelHook
  402. LodelHook.call_hook(
  403. "lodel2_plugins_loaded", cls, cls._plugin_instances)
  404. ##@return a copy of __confspecs attr
  405. @property
  406. def confspecs(self):
  407. return copy.copy(self.__confspecs)
  408. ##@brief Attempt to read plugin discover cache
  409. #@note If no cache yet make a discover with default plugin directory
  410. #@return a dict (see @ref _discover() )
  411. @classmethod
  412. def plugin_cache(cls):
  413. if cls._discover_cache is None:
  414. if not os.path.isfile(DISCOVER_CACHE_FILENAME):
  415. cls.discover()
  416. with open(DISCOVER_CACHE_FILENAME) as pdcache_fd:
  417. res = json.load(pdcache_fd)
  418. #Check consistency of loaded cache
  419. if 'path_list' not in res:
  420. raise LodelFatalError("Malformed plugin's discover cache file \
  421. : '%s'. Unable to find plugin's paths list." % DISCOVER_CACHE_FILENAME)
  422. expected_keys = ['type', 'path', 'version']
  423. for pname in res['plugins']:
  424. for ekey in expected_keys:
  425. if ekey not in res['plugins'][pname]:
  426. #Bad cache !
  427. logger.warning("Malformed plugin's discover cache \
  428. file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
  429. cls._discover_cache = cls.discover(res['path_list'])
  430. break
  431. else:
  432. #The cache we just read was OK
  433. cls._discover_cache = res
  434. return cls._discover_cache
  435. ##@brief Register a new plugin
  436. #
  437. #@param plugin_name str : The plugin name
  438. #@return a Plugin instance
  439. #@throw PluginError
  440. @classmethod
  441. def register(cls, plugin_name):
  442. from .datasource_plugin import DatasourcePlugin
  443. if plugin_name in cls._plugin_instances:
  444. msg = "Plugin allready registered with same name %s"
  445. msg %= plugin_name
  446. raise PluginError(msg)
  447. #Here we check that previous discover found a plugin with that name
  448. pdcache = cls.plugin_cache()
  449. if plugin_name not in pdcache['plugins']:
  450. raise PluginError("No plugin named %s found" % plugin_name)
  451. pinfos = pdcache['plugins'][plugin_name]
  452. ptype = pinfos['type']
  453. if ptype == 'datasource':
  454. pcls = DatasourcePlugin
  455. else:
  456. pcls = cls
  457. print(plugin_name, ptype, pcls)
  458. plugin = pcls(plugin_name)
  459. cls._plugin_instances[plugin_name] = plugin
  460. logger.debug("Plugin %s available." % plugin)
  461. return plugin
  462. ##@brief Plugins instances accessor
  463. #
  464. #@param plugin_name str: The plugin name
  465. #@return a Plugin instance
  466. #@throw PluginError if plugin not found
  467. @classmethod
  468. def get(cls, plugin_name):
  469. try:
  470. return cls._plugin_instances[plugin_name]
  471. except KeyError:
  472. msg = "No plugin named '%s' loaded"
  473. msg %= plugin_name
  474. raise PluginError(msg)
  475. ##@brief Given a plugin name returns the plugin path
  476. # @param plugin_name str : The plugin name
  477. # @return the plugin directory path
  478. @classmethod
  479. def plugin_path(cls, plugin_name):
  480. cls.started()
  481. plist = cls.plugin_list()
  482. if plugin_name not in plist:
  483. raise PluginError("No plugin named '%s' found" % plugin_name)
  484. try:
  485. return cls.get(plugin_name).path
  486. except PluginError:
  487. pass
  488. return plist[plugin_name]['path']
  489. @classmethod
  490. def plugin_module_name(cls, plugin_name):
  491. return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
  492. ##@brief Start the Plugin class
  493. #
  494. # Called by Settings.__bootstrap()
  495. #
  496. # This method load path and preload plugins
  497. @classmethod
  498. def start(cls, plugins_directories, plugins):
  499. if cls._plugin_directories is not None:
  500. return
  501. import inspect
  502. self_path = inspect.getsourcefile(Plugin)
  503. default_plugin_path = os.path.abspath(self_path + '../../../../plugins')
  504. if plugins_directories is None:
  505. plugins_directories = list()
  506. plugins_directories += [ default_plugin_path ]
  507. cls._plugin_directories = list(set(plugins_directories))
  508. for plugin_name in plugins:
  509. cls.register(plugin_name)
  510. @classmethod
  511. def started(cls, raise_if_not = True):
  512. res = cls._plugin_directories is not None
  513. if raise_if_not and not res:
  514. raise RuntimeError("Class Plugins is not initialized")
  515. @classmethod
  516. def clear(cls):
  517. if cls._plugin_directories is not None:
  518. cls._plugin_directories = None
  519. if cls._plugin_instances != dict():
  520. cls._plugin_instances = dict()
  521. if cls._load_called != []:
  522. cls._load_called = []
  523. ##@brief Reccursively walk throught paths to find plugin, then stores
  524. #found plugin in a file...
  525. #@param paths list : list of directory paths
  526. #@param no_cache bool : if true only return a list of found plugins
  527. #without modifying the cache file
  528. #@return a dict {'path_list': [...], 'plugins': { see @ref _discover }}
  529. #@todo add max_depth and symlink following options
  530. @classmethod
  531. def discover(cls, paths = None, no_cache = False):
  532. logger.info("Running plugin discover")
  533. if paths is None:
  534. paths = DEFAULT_PLUGINS_PATH_LIST
  535. tmp_res = []
  536. for path in paths:
  537. tmp_res += cls._discover(path)
  538. #Formating and dedoubloning result
  539. result = dict()
  540. for pinfos in tmp_res:
  541. pname = pinfos['name']
  542. if ( pname in result
  543. and pinfos['version'] > result[pname]['version'])\
  544. or pname not in result:
  545. result[pname] = pinfos
  546. else:
  547. #dropped
  548. pass
  549. result = {'path_list': paths, 'plugins': result}
  550. #Writing to cache
  551. if not no_cache:
  552. with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache:
  553. pdcache.write(json.dumps(result))
  554. return result
  555. ##@brief Return discover result
  556. #@param refresh bool : if true invalidate all plugin list cache
  557. #@note If discover cache file not found run discover first
  558. #@note if refresh is set to True discover MUST have been run at least
  559. #one time. In fact refresh action load the list of path to explore
  560. #from the plugin's discover cache
  561. @classmethod
  562. def plugin_list(cls, refresh = False):
  563. try:
  564. infos = cls._load_discover_cache()
  565. path_list = infos['path_list']
  566. except PluginError:
  567. refresh = True
  568. path_list = DEFAULT_PLUGINS_PATH_LIST
  569. if cls._plugin_list is None or refresh:
  570. if not os.path.isfile(DISCOVER_CACHE_FILENAME) or refresh:
  571. infos = cls.discover(path_list)
  572. cls._plugin_list = infos['plugins']
  573. return cls._plugin_list
  574. ##@brief Attempt to open and load plugin discover cache
  575. #@return discover cache
  576. #@throw PluginError when open or load fails
  577. @classmethod
  578. def _load_discover_cache(cls):
  579. try:
  580. pdcache = open(DISCOVER_CACHE_FILENAME, 'r')
  581. except Exception as e:
  582. msg = "Unable to open discover cache : %s"
  583. msg %= e
  584. raise PluginError(msg)
  585. try:
  586. res = json.load(pdcache)
  587. except Exception as e:
  588. msg = "Unable to load discover cache : %s"
  589. msg %= e
  590. raise PluginError(msg)
  591. pdcache.close()
  592. return res
  593. ##@brief Check if a directory is a plugin module
  594. #@param path str : path to check
  595. #@return a dict with name, version and path if path is a plugin module, else False
  596. @classmethod
  597. def dir_is_plugin(cls, path):
  598. log_msg = "%s is not a plugin directory because : " % path
  599. #Checks that path exists
  600. if not os.path.isdir(path):
  601. raise ValueError(
  602. "Expected path to be a directory, but '%s' found" % path)
  603. #Checks that path contains plugin's init file
  604. initfile = os.path.join(path, INIT_FILENAME)
  605. if not os.path.isfile(initfile):
  606. log_msg += "'%s' not found" % (INIT_FILENAME)
  607. logger.debug(log_msg)
  608. return False
  609. #Importing plugin's init file to check contained datas
  610. try:
  611. initmod, modname = cls.import_init(path)
  612. except PluginError as e:
  613. log_msg += "unable to load '%s'. Exception raised : %s"
  614. log_msg %= (INIT_FILENAME, e)
  615. logger.debug(log_msg)
  616. return False
  617. #Checking mandatory init module variables
  618. for attr_name in MANDATORY_VARNAMES:
  619. if not hasattr(initmod,attr_name):
  620. log_msg += " mandatory variable '%s' not found in '%s'"
  621. log_msg %= (attr_name, INIT_FILENAME)
  622. logger.debug(log_msg)
  623. return False
  624. #Fetching plugin's version
  625. try:
  626. pversion = getattr(initmod, PLUGIN_VERSION_VARNAME)
  627. except (NameError, AttributeError) as e:
  628. msg = "Invalid plugin version found in %s : %s"
  629. msg %= (path, e)
  630. raise PluginError(msg)
  631. #Fetching plugin's type
  632. try:
  633. ptype = getattr(initmod, PLUGIN_TYPE_VARNAME)
  634. except (NameError, AttributeError) as e:
  635. ptype = DEFAULT_PLUGIN_TYPE
  636. pname = getattr(initmod, PLUGIN_NAME_VARNAME)
  637. return {'name': pname,
  638. 'version': pversion,
  639. 'path': path,
  640. 'type': ptype}
  641. ##@brief Import init file from a plugin path
  642. #@param path str : Directory path
  643. #@return a tuple (init_module, module_name)
  644. @classmethod
  645. def import_init(self, path):
  646. init_source = os.path.join(path, INIT_FILENAME)
  647. temp_module = '%s.%s.%s' % (
  648. VIRTUAL_TEMP_PACKAGE_NAME, os.path.basename(os.path.dirname(path)),
  649. 'test_init')
  650. try:
  651. loader = SourceFileLoader(temp_module, init_source)
  652. except (ImportError, FileNotFoundError) as e:
  653. raise PluginError("Unable to import init file from '%s' : %s" % (
  654. temp_module, e))
  655. try:
  656. res_module = loader.load_module()
  657. except Exception as e:
  658. raise PluginError("Unable to import initfile")
  659. return (res_module, temp_module)
  660. ##@brief Reccursiv plugin discover given a path
  661. #@param path str : the path to walk through
  662. #@return A dict with plugin_name as key and {'path':..., 'version':...} as value
  663. @classmethod
  664. def _discover(cls, path):
  665. res = []
  666. to_explore = [path]
  667. while len(to_explore) > 0:
  668. cur_path = to_explore.pop()
  669. for f in os.listdir(cur_path):
  670. f_path = os.path.join(cur_path, f)
  671. if f not in ['.', '..'] and os.path.isdir(f_path):
  672. #Check if it is a plugin directory
  673. test_result = cls.dir_is_plugin(f_path)
  674. if not (test_result is False):
  675. logger.info("Plugin found in %s" % f_path)
  676. res.append(test_result)
  677. else:
  678. to_explore.append(f_path)
  679. return res
  680. ##@brief Decorator class designed to allow plugins to add custom methods
  681. #to LeObject childs (dyncode objects)
  682. #
  683. class CustomMethod(object):
  684. ##@brief Stores registered custom methods
  685. #
  686. #Key = LeObject child class name
  687. #Value = CustomMethod instance
  688. _custom_methods = dict()
  689. INSTANCE_METHOD = 0
  690. CLASS_METHOD = 1
  691. STATIC_METHOD = 2
  692. ##@brief Decorator constructor
  693. #@param component_name str : the name of the component to enhance
  694. #@param method_name str : the name of the method to inject (if None given
  695. #@param method_type int : take value in one of
  696. #CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD or
  697. #CustomMethod::STATIC_METHOD
  698. #use the function name
  699. def __init__(self, component_name, method_name = None, method_type=0):
  700. ##@brief The targeted LeObject child class
  701. self._comp_name = component_name
  702. ##@brief The method name
  703. self._method_name = method_name
  704. ##@brief The function (that will be the injected method)
  705. self._fun = None
  706. ##@brief Stores the type of method (instance, class or static)
  707. self._type = int(method_type)
  708. if self._type not in (self.INSTANCE_METHOD, self.CLASS_METHOD,\
  709. self.STATIC_METHOD):
  710. raise ValueError("Excepted value for method_type was one of \
  711. CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD or \
  712. CustomMethod::STATIC_METHOD, but got %s" % self._type)
  713. ##@brief called just after __init__
  714. #@param fun function : the decorated function
  715. #@param return the function
  716. def __call__(self, fun):
  717. if self._method_name is None:
  718. self._method_name = fun.__name__
  719. if self._comp_name not in self._custom_methods:
  720. self._custom_methods[self._comp_name] = list()
  721. if self._method_name in [ scm._method_name for scm in self._custom_methods[self._comp_name]]:
  722. raise RuntimeError("A method named %s allready registered by \
  723. another plugin : %s" % (
  724. self._method_name,
  725. self._custom_methods[self._comp_name].__module__))
  726. self._fun = fun
  727. self._custom_methods[self._comp_name].append(self)
  728. ##@brief Textual representation
  729. #@return textual representation of the CustomMethod instance
  730. def __repr__(self):
  731. res = "<CustomMethod name={method_name} target={classname} \
  732. source={module_name}.{fun_name}>"
  733. return res.format(
  734. method_name = self._method_name,
  735. classname = self._comp_name,
  736. module_name = self._fun.__module__,
  737. fun_name = self._fun.__name__)
  738. ##@brief Return a well formed method
  739. #
  740. #@note the type of method depends on the _type attribute
  741. #@return a method directly injectable in the target class
  742. def __get_method(self):
  743. if self._type == self.INSTANCE_METHOD:
  744. def custom__get__(self, obj, objtype = None):
  745. return types.MethodType(self, obj, objtype)
  746. setattr(self._fun, '__get__', custom__get__)
  747. return self._fun
  748. elif self._type == self.CLASS_METHOD:
  749. return classmethod(self._fun)
  750. elif self._type == self.STATIC_METHOD:
  751. return staticmethod(self._fun)
  752. else:
  753. raise RuntimeError("Attribute _type is not one of \
  754. CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD \
  755. CustomMethod::STATIC_METHOD")
  756. ##@brief Handle custom method dynamic injection in LeAPI dyncode
  757. #
  758. #Called by lodel2_dyncode_loaded hook defined at
  759. #lodel.plugin.core_hooks.lodel2_plugin_custom_methods()
  760. #
  761. #@param cls
  762. #@param dynclasses LeObject child classes : List of dynamically generated
  763. #LeObject child classes
  764. @classmethod
  765. def set_registered(cls, dynclasses):
  766. from lodel import logger
  767. dyn_cls_dict = { dc.__name__:dc for dc in dynclasses}
  768. for cls_name, custom_methods in cls._custom_methods.items():
  769. for custom_method in custom_methods:
  770. if cls_name not in dyn_cls_dict:
  771. logger.error("Custom method %s adding fails : No dynamic \
  772. LeAPI objects named %s." % (custom_method, cls_name))
  773. elif custom_method._method_name in dir(dyn_cls_dict[cls_name]):
  774. logger.warning("Overriding existing method '%s' on target \
  775. with %s" % (custom_method._method_name, custom_method))
  776. else:
  777. setattr(
  778. dyn_cls_dict[cls_name],
  779. custom_method._method_name,
  780. custom_method.__get_method())
  781. logger.debug(
  782. "Custom method %s added to target" % custom_method)
  783. ##@page lodel2_plugins Lodel2 plugins system
  784. #
  785. # @par Plugin structure
  786. #A plugin is a package (a folder containing, at least, an __init__.py file.
  787. #This file should expose multiple things :
  788. # - a CONFSPEC variable containing configuration specifications
  789. # - an _activate() method that returns True if the plugin can be activated (
  790. # optionnal)
  791. #
  792. class SessionHandler(Plugin):
  793. __instance = None
  794. def __init__(self, plugin_name):
  795. if self.__instance is None:
  796. super(Plugin, self).__init__(plugin_name)
  797. self.__instance = True
  798. else:
  799. raise RuntimeError("A SessionHandler Plugin is already plug")
  800. class InterfacePlugin(Plugin):
  801. def __init__(self, plugin_name):
  802. super(Plugin, self).__init__(plugin_name)