|
@@ -4,6 +4,7 @@ import sys
|
4
|
4
|
import os.path
|
5
|
5
|
import importlib
|
6
|
6
|
import copy
|
|
7
|
+import json
|
7
|
8
|
from importlib.machinery import SourceFileLoader, SourcelessFileLoader
|
8
|
9
|
|
9
|
10
|
import plugins
|
|
@@ -19,12 +20,131 @@ from .exceptions import *
|
19
|
20
|
|
20
|
21
|
##@brief The package in which we will load plugins modules
|
21
|
22
|
VIRTUAL_PACKAGE_NAME = 'lodel.plugins'
|
|
23
|
+##@brief The temporary package to import python sources
|
|
24
|
+VIRTUAL_TEMP_PACKAGE_NAME = 'lodel.plugin_tmp'
|
|
25
|
+##@brief Plugin init filename
|
22
|
26
|
INIT_FILENAME = '__init__.py' # Loaded with settings
|
|
27
|
+PLUGIN_NAME_VARNAME = '__plugin_name__'
|
|
28
|
+PLUGIN_TYPE_VARNAME = '__type__'
|
|
29
|
+PLUGIN_VERSION_VARNAME = '__version__'
|
23
|
30
|
CONFSPEC_FILENAME_VARNAME = '__confspec__'
|
24
|
31
|
CONFSPEC_VARNAME = 'CONFSPEC'
|
25
|
32
|
LOADER_FILENAME_VARNAME = '__loader__'
|
26
|
33
|
PLUGIN_DEPS_VARNAME = '__plugin_deps__'
|
27
|
34
|
ACTIVATE_METHOD_NAME = '_activate'
|
|
35
|
+##@brief Discover stage cache filename
|
|
36
|
+DISCOVER_CACHE_FILENAME = '.plugin_discover_cache.json'
|
|
37
|
+##@brief Default & failover value for plugins path list
|
|
38
|
+DEFAULT_PLUGINS_PATH_LIST = ['./plugins']
|
|
39
|
+
|
|
40
|
+MANDATORY_VARNAMES = [PLUGIN_NAME_VARNAME, LOADER_FILENAME_VARNAME,
|
|
41
|
+ PLUGIN_VERSION_VARNAME]
|
|
42
|
+
|
|
43
|
+PLUGIN_DEFAULT_TYPE = 'default'
|
|
44
|
+PLUGINS_TYPES = [PLUGIN_DEFAULT_TYPE, 'datasource', 'session_handler']
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+##@brief Describe and handle version numbers
|
|
48
|
+class PluginVersion(object):
|
|
49
|
+
|
|
50
|
+ PROPERTY_LIST = ['major', 'minor', 'revision' ]
|
|
51
|
+
|
|
52
|
+ ##@brief Version constructor
|
|
53
|
+ #@param *args : You can either give a str that will be splitted on . or you
|
|
54
|
+ #can give a iterable containing 3 integer or 3 arguments representing
|
|
55
|
+ #major, minor and revision version
|
|
56
|
+ def __init__(self, *args):
|
|
57
|
+ self.__version = [0 for _ in range(3) ]
|
|
58
|
+ if len(args) == 1:
|
|
59
|
+ arg = args[0]
|
|
60
|
+ if isinstance(arg, str):
|
|
61
|
+ spl = arg.split('.')
|
|
62
|
+ invalid = False
|
|
63
|
+ if len(spl) > 3:
|
|
64
|
+ raise PluginError("The string '%s' is not a valid plugin \
|
|
65
|
+version number" % arg)
|
|
66
|
+ else:
|
|
67
|
+ try:
|
|
68
|
+ if len(arg) >= 1:
|
|
69
|
+ if len(arg) > 3:
|
|
70
|
+ raise PluginError("Expected maximum 3 value to \
|
|
71
|
+create a plugin version number but found '%s' as argument" % arg)
|
|
72
|
+ for i, v in enumerate(arg):
|
|
73
|
+ self.__version[i] = arg[i]
|
|
74
|
+ except TypeError:
|
|
75
|
+ raise PluginError("Unable to convert argument into plugin \
|
|
76
|
+version number" % arg)
|
|
77
|
+ elif len(args) > 3:
|
|
78
|
+ raise PluginError("Expected between 1 and 3 positional arguments \
|
|
79
|
+but %d arguments found" % len(args))
|
|
80
|
+ else:
|
|
81
|
+ for i,v in enumerate(args):
|
|
82
|
+ self.__version[i] = v
|
|
83
|
+
|
|
84
|
+ @property
|
|
85
|
+ def major(self):
|
|
86
|
+ return self.__version[0]
|
|
87
|
+
|
|
88
|
+ @property
|
|
89
|
+ def minor(self):
|
|
90
|
+ return self.__version[1]
|
|
91
|
+
|
|
92
|
+ @property
|
|
93
|
+ def revision(self):
|
|
94
|
+ return self.__version[2]
|
|
95
|
+
|
|
96
|
+ ##@brief Check and prepare comparisoon argument
|
|
97
|
+ #@return A PluginVersion instance
|
|
98
|
+ #@throw PluginError if invalid argument provided
|
|
99
|
+ def __cmp_check(self, other):
|
|
100
|
+ if not isinstance(other, PluginVersion):
|
|
101
|
+ try:
|
|
102
|
+ if len(other) <= 3 and len(other) > 0:
|
|
103
|
+ return PluginVersion(other)
|
|
104
|
+ except TypeError:
|
|
105
|
+ raise PluginError("Cannot compare argument '%s' with \
|
|
106
|
+a PluginVerison instance" % other)
|
|
107
|
+ return other
|
|
108
|
+
|
|
109
|
+ ##@brief Generic comparison function
|
|
110
|
+ #@param other PluginVersion or iterable
|
|
111
|
+ #@param cmp_fun_name function : interger comparison function
|
|
112
|
+ def __generic_cmp(self, other, cmp_fun_name):
|
|
113
|
+ other = self.__cmp_check(other)
|
|
114
|
+ try:
|
|
115
|
+ cmpfun = getattr(int, cmp_fun_name)
|
|
116
|
+ except AttributeError:
|
|
117
|
+ raise LodelFatalError("Invalid comparison callback given \
|
|
118
|
+to generic PluginVersion comparison function : '%s'" % cmp_fun_name)
|
|
119
|
+ for property_name in self.PROPERTY_LIST:
|
|
120
|
+ if not cmpfun(getattr(self, pname), getattr(other, pname)):
|
|
121
|
+ return False
|
|
122
|
+ return True
|
|
123
|
+
|
|
124
|
+ def __lt__(self, other):
|
|
125
|
+ return self.__generic_cmp(other, '__lt__')
|
|
126
|
+
|
|
127
|
+ def __le__(self, other):
|
|
128
|
+ return self.__generic_cmp(other, '__le__')
|
|
129
|
+
|
|
130
|
+ def __eq__(self, other):
|
|
131
|
+ return self.__generic_cmp(other, '__eq__')
|
|
132
|
+
|
|
133
|
+ def __ne__(self, other):
|
|
134
|
+ return self.__generic_cmp(other, '__ne__')
|
|
135
|
+
|
|
136
|
+ def __gt__(self, other):
|
|
137
|
+ return self.__generic_cmp(other, '__gt__')
|
|
138
|
+
|
|
139
|
+ def __ge__(self, other):
|
|
140
|
+ return self.__generic_cmp(other, '__ge__')
|
|
141
|
+
|
|
142
|
+ def __str__(self):
|
|
143
|
+ return '%d.%d.%d' % tuple(self.__version)
|
|
144
|
+
|
|
145
|
+ def __repr__(self):
|
|
146
|
+ return {'major': self.major, 'minor': self.minor,
|
|
147
|
+ 'revision': self.revision}
|
28
|
148
|
|
29
|
149
|
|
30
|
150
|
##@brief Handle plugins
|
|
@@ -48,6 +168,9 @@ class Plugin(object):
|
48
|
168
|
#dependencies
|
49
|
169
|
_load_called = []
|
50
|
170
|
|
|
171
|
+ ##@brief Attribute that stores plugins list from discover cache file
|
|
172
|
+ _plugin_list = None
|
|
173
|
+
|
51
|
174
|
##@brief Plugin class constructor
|
52
|
175
|
#
|
53
|
176
|
# Called by setting in early stage of lodel2 boot sequence using classmethod
|
|
@@ -67,11 +190,11 @@ class Plugin(object):
|
67
|
190
|
self.__confspecs = dict()
|
68
|
191
|
self.loaded = False
|
69
|
192
|
|
70
|
|
- # Importing __init__.py
|
|
193
|
+ # Importing __init__.py infos in it
|
71
|
194
|
plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
|
72
|
195
|
plugin_name)
|
73
|
196
|
|
74
|
|
- init_source = self.path + INIT_FILENAME
|
|
197
|
+ init_source = os.path.join(self.path, INIT_FILENAME)
|
75
|
198
|
try:
|
76
|
199
|
loader = SourceFileLoader(plugin_module, init_source)
|
77
|
200
|
self.module = loader.load_module()
|
|
@@ -109,23 +232,39 @@ class Plugin(object):
|
109
|
232
|
varname = CONFSPEC_VARNAME,
|
110
|
233
|
filename = confspec_filename)
|
111
|
234
|
raise PluginError(msg)
|
|
235
|
+ # loading plugin version
|
|
236
|
+ try:
|
|
237
|
+ #this try block should be useless. The existance of
|
|
238
|
+ #PLUGIN_VERSION_VARNAME in init file is mandatory
|
|
239
|
+ self.__version = getattr(self.module, PLUGIN_VERSION_VARNAME)
|
|
240
|
+ except AttributeError:
|
|
241
|
+ msg = "Error that should not append : no %s found in plugin \
|
|
242
|
+init file. Malformed plugin"
|
|
243
|
+ msg %= PLUGIN_VERSION_VARNAME
|
|
244
|
+ raise LodelFatalError(msg)
|
112
|
245
|
|
113
|
|
- ##@brief Browse directory to get plugin
|
114
|
|
- #@param plugin_path
|
115
|
|
- #@return module existing
|
116
|
|
- def _discover_plugin(self, plugin_path):
|
117
|
|
- res = os.listdir(plugin_path) is not None
|
118
|
|
- if res:
|
119
|
|
- dirname = os.path.dirname(plugin_path)
|
120
|
|
- for f in os.listdir(plugin_path):
|
121
|
|
- file_name = ''.join(dirname, f)
|
122
|
|
- if self.is_plugin_dir(file_name):
|
123
|
|
- return self.is_plugin_dir(file_name)
|
124
|
|
- else:
|
125
|
|
- self._discover_plugin(file_name)
|
126
|
|
- else:
|
127
|
|
- pass
|
128
|
|
-
|
|
246
|
+ # Load plugin type
|
|
247
|
+ try:
|
|
248
|
+ self.__type = getattr(self.module, PLUGIN_TYPE_VARNAME)
|
|
249
|
+ except AttributeError:
|
|
250
|
+ self.__type = PLUGIN_DEFAULT_TYPE
|
|
251
|
+ self.__type = str(self.__type).lower()
|
|
252
|
+ if self.__type not in PLUGINS_TYPES:
|
|
253
|
+ raise PluginError("Unknown plugin type '%s'" % self.__type)
|
|
254
|
+ # Load plugin name from init file (just for checking)
|
|
255
|
+ try:
|
|
256
|
+ #this try block should be useless. The existance of
|
|
257
|
+ #PLUGIN_NAME_VARNAME in init file is mandatory
|
|
258
|
+ pname = getattr(self.module, PLUGIN_NAME_VARNAME)
|
|
259
|
+ except AttributeError:
|
|
260
|
+ msg = "Error that should not append : no %s found in plugin \
|
|
261
|
+init file. Malformed plugin"
|
|
262
|
+ msg %= PLUGIN_NAME_VARNAME
|
|
263
|
+ raise LodelFatalError(msg)
|
|
264
|
+ if pname != plugin_name:
|
|
265
|
+ msg = "Plugin's discover cache inconsistency detected ! Cached \
|
|
266
|
+name differ from the one found in plugin's init file"
|
|
267
|
+ raise PluginError(msg)
|
129
|
268
|
|
130
|
269
|
##@brief Try to import a file from a variable in __init__.py
|
131
|
270
|
#@param varname str : The variable name
|
|
@@ -153,7 +292,7 @@ class Plugin(object):
|
153
|
292
|
raise PluginError(msg)
|
154
|
293
|
# importing the file in varname
|
155
|
294
|
module_name = self.module.__name__+"."+varname
|
156
|
|
- filename = self.path + filename
|
|
295
|
+ filename = os.path.join(self.path, filename)
|
157
|
296
|
loader = SourceFileLoader(module_name, filename)
|
158
|
297
|
return loader.load_module()
|
159
|
298
|
|
|
@@ -252,6 +391,9 @@ class Plugin(object):
|
252
|
391
|
raise RuntimeError("Plugin %s not loaded yet."%self.name)
|
253
|
392
|
return self.__loader_module
|
254
|
393
|
|
|
394
|
+ def __str__(self):
|
|
395
|
+ return "<LodelPlugin '%s' version %s>" % (self.name, self.__version)
|
|
396
|
+
|
255
|
397
|
##@brief Call load method on every pre-loaded plugins
|
256
|
398
|
#
|
257
|
399
|
# Called by loader to trigger hooks registration.
|
|
@@ -317,18 +459,17 @@ class Plugin(object):
|
317
|
459
|
@classmethod
|
318
|
460
|
def plugin_path(cls, plugin_name):
|
319
|
461
|
cls.started()
|
|
462
|
+ plist = cls.plugin_list()
|
|
463
|
+ if plugin_name not in plist:
|
|
464
|
+ raise PluginError("No plugin named '%s' found" % plugin_name)
|
|
465
|
+
|
320
|
466
|
try:
|
321
|
467
|
return cls.get(plugin_name).path
|
322
|
468
|
except PluginError:
|
323
|
469
|
pass
|
324
|
|
-
|
325
|
|
- path = None
|
326
|
|
- for cur_path in cls._plugin_directories:
|
327
|
|
- plugin_path = os.path.join(cur_path, plugin_name)+'/'
|
328
|
|
- if os.path.isdir(plugin_path):
|
329
|
|
- return plugin_path
|
330
|
|
- raise NameError("No plugin named '%s'" % plugin_name)
|
331
|
470
|
|
|
471
|
+ return plist[plugin_name]['path']
|
|
472
|
+
|
332
|
473
|
@classmethod
|
333
|
474
|
def plugin_module_name(cls, plugin_name):
|
334
|
475
|
return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
|
|
@@ -366,6 +507,164 @@ class Plugin(object):
|
366
|
507
|
cls._plugin_instances = dict()
|
367
|
508
|
if cls._load_called != []:
|
368
|
509
|
cls._load_called = []
|
|
510
|
+
|
|
511
|
+ @classmethod
|
|
512
|
+ ##@brief Browse directory to get plugin
|
|
513
|
+ #@param plugin_path
|
|
514
|
+ #@return module existing
|
|
515
|
+ def plugin_discover(self, plugin_path):
|
|
516
|
+ res = os.listdir(plugin_path) is not None
|
|
517
|
+ if res:
|
|
518
|
+ dirname = os.path.dirname(plugin_path)
|
|
519
|
+ for f in os.listdir(plugin_path):
|
|
520
|
+ file_name = ''.join(dirname, f)
|
|
521
|
+ if self.is_plugin_dir(file_name):
|
|
522
|
+ return self.is_plugin_dir(file_name)
|
|
523
|
+ else:
|
|
524
|
+ self._discover_plugin(file_name)
|
|
525
|
+ else:
|
|
526
|
+ pass
|
|
527
|
+
|
|
528
|
+ ##@brief Reccursively walk throught paths to find plugin, then stores
|
|
529
|
+ #found plugin in a file...
|
|
530
|
+ #@return a dict {'path_list': [...], 'plugins': { see @ref _discover }}
|
|
531
|
+ @classmethod
|
|
532
|
+ def discover(cls, paths):
|
|
533
|
+ tmp_res = []
|
|
534
|
+ for path in paths:
|
|
535
|
+ tmp_res += cls._discover(path)
|
|
536
|
+ #Formating and dedoubloning result
|
|
537
|
+ result = dict()
|
|
538
|
+ for pinfos in tmp_res:
|
|
539
|
+ pname = pinfos['name']
|
|
540
|
+ if (
|
|
541
|
+ pname in result
|
|
542
|
+ and pinfos['version'] > result[pname]['version'])\
|
|
543
|
+ or pname not in result:
|
|
544
|
+ result[pname] = pinfos
|
|
545
|
+ else:
|
|
546
|
+ #dropped
|
|
547
|
+ pass
|
|
548
|
+ result = {'path_list': paths, 'plugins': result}
|
|
549
|
+ #Writing to cache
|
|
550
|
+ with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache:
|
|
551
|
+ pdcache.write(json.dumps(result))
|
|
552
|
+ return result
|
|
553
|
+
|
|
554
|
+ ##@brief Return discover result
|
|
555
|
+ #@param refresh bool : if true invalidate all plugin list cache
|
|
556
|
+ #@note If discover cache file not found run discover first
|
|
557
|
+ #@note if refresh is set to True discover MUST have been run at least
|
|
558
|
+ #one time. In fact refresh action load the list of path to explore
|
|
559
|
+ #from the plugin's discover cache
|
|
560
|
+ @classmethod
|
|
561
|
+ def plugin_list(cls, refresh = False):
|
|
562
|
+ try:
|
|
563
|
+ infos = cls._load_discover_cache()
|
|
564
|
+ path_list = infos['path_list']
|
|
565
|
+ except PluginError:
|
|
566
|
+ refresh = True
|
|
567
|
+ path_list = DEFAULT_PLUGINS_PATH_LIST
|
|
568
|
+
|
|
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
|
+
|
|
575
|
+ ##@brief Attempt to open and load plugin discover cache
|
|
576
|
+ #@return discover cache
|
|
577
|
+ #@throw PluginError when open or load fails
|
|
578
|
+ @classmethod
|
|
579
|
+ def _load_discover_cache(cls):
|
|
580
|
+ try:
|
|
581
|
+ pdcache = open(DISCOVER_CACHE_FILENAME, 'r')
|
|
582
|
+ except Exception as e:
|
|
583
|
+ msg = "Unable to open discover cache : %s"
|
|
584
|
+ msg %= e
|
|
585
|
+ raise PluginError(msg)
|
|
586
|
+ try:
|
|
587
|
+ res = json.load(pdcache)
|
|
588
|
+ except Exception as e:
|
|
589
|
+ msg = "Unable to load discover cache : %s"
|
|
590
|
+ msg %= e
|
|
591
|
+ raise PluginError(msg)
|
|
592
|
+ pdcache.close()
|
|
593
|
+ return res
|
|
594
|
+
|
|
595
|
+ ##@brief Check if a directory is a plugin module
|
|
596
|
+ #@param path str : path to check
|
|
597
|
+ #@return a dict with name, version and path if path is a plugin module, else False
|
|
598
|
+ @classmethod
|
|
599
|
+ def dir_is_plugin(cls, path):
|
|
600
|
+ #Checks that path exists
|
|
601
|
+ if not os.path.isdir(path):
|
|
602
|
+ raise ValueError(
|
|
603
|
+ "Expected path to be a directory, but '%s' found" % path)
|
|
604
|
+ #Checks that path contains plugin's init file
|
|
605
|
+ initfile = os.path.join(path, INIT_FILENAME)
|
|
606
|
+ if not os.path.isfile(initfile):
|
|
607
|
+ return False
|
|
608
|
+ #Importing plugin's init file to check contained datas
|
|
609
|
+ try:
|
|
610
|
+ initmod, modname = cls.import_init(path)
|
|
611
|
+ except PluginError:
|
|
612
|
+ return False
|
|
613
|
+ #Checking mandatory init module variables
|
|
614
|
+ for attr_name in MANDATORY_VARNAMES:
|
|
615
|
+ if not hasattr(initmod,attr_name):
|
|
616
|
+ return False
|
|
617
|
+ try:
|
|
618
|
+ pversion = getattr(initmod, PLUGIN_VERSION_VARNAME)
|
|
619
|
+ except PluginError as e:
|
|
620
|
+ msg = "Invalid plugin version found in %s : %s"
|
|
621
|
+ msg %= (path, e)
|
|
622
|
+ raise PluginError(msg)
|
|
623
|
+ pname = getattr(initmod, PLUGIN_NAME_VARNAME)
|
|
624
|
+ return {'name': pname,
|
|
625
|
+ 'version': pversion,
|
|
626
|
+ 'path': path}
|
|
627
|
+
|
|
628
|
+ ##@brief Import init file from a plugin path
|
|
629
|
+ #@param path str : Directory path
|
|
630
|
+ #@return a tuple (init_module, module_name)
|
|
631
|
+ @classmethod
|
|
632
|
+ def import_init(self, path):
|
|
633
|
+ init_source = os.path.join(path, INIT_FILENAME)
|
|
634
|
+ temp_module = '%s.%s.%s' % (
|
|
635
|
+ VIRTUAL_TEMP_PACKAGE_NAME, os.path.basename(os.path.dirname(path)),
|
|
636
|
+ 'test_init')
|
|
637
|
+ try:
|
|
638
|
+ loader = SourceFileLoader(temp_module, init_source)
|
|
639
|
+ except (ImportError, FileNotFoundError) as e:
|
|
640
|
+ raise PluginError("Unable to import init file from '%s' : %s" % (
|
|
641
|
+ temp_module, e))
|
|
642
|
+ try:
|
|
643
|
+ res_module = loader.load_module()
|
|
644
|
+ except Exception as e:
|
|
645
|
+ raise PluginError("Unable to import initfile")
|
|
646
|
+ return (res_module, temp_module)
|
|
647
|
+
|
|
648
|
+ ##@brief Reccursiv plugin discover given a path
|
|
649
|
+ #@param path str : the path to walk through
|
|
650
|
+ #@return A dict with plugin_name as key and {'path':..., 'version':...} as value
|
|
651
|
+ @classmethod
|
|
652
|
+ def _discover(cls, path):
|
|
653
|
+ res = []
|
|
654
|
+ to_explore = [path]
|
|
655
|
+ while len(to_explore) > 0:
|
|
656
|
+ cur_path = to_explore.pop()
|
|
657
|
+ for f in os.listdir(cur_path):
|
|
658
|
+ f_path = os.path.join(cur_path, f)
|
|
659
|
+ if f not in ['.', '..'] and os.path.isdir(f_path):
|
|
660
|
+ #Check if it is a plugin directory
|
|
661
|
+ test_result = cls.dir_is_plugin(f_path)
|
|
662
|
+ if not (test_result is False):
|
|
663
|
+ res.append(test_result)
|
|
664
|
+ else:
|
|
665
|
+ to_explore.append(f_path)
|
|
666
|
+ return res
|
|
667
|
+
|
369
|
668
|
|
370
|
669
|
##@brief Decorator class designed to allow plugins to add custom methods
|
371
|
670
|
#to LeObject childs (dyncode objects)
|