38
38
import zipimport
39
39
from concurrent import futures
40
40
from functools import wraps , partial
41
+ from itertools import chain
41
42
42
43
from .i18n import _
43
44
from .util import (profiler , DaemonThread , UserCancelled , ThreadJob , UserFacingException )
@@ -91,9 +92,9 @@ def __init__(self, config: SimpleConfig, gui_name = None, cmd_only: bool = False
91
92
def descriptions (self ):
92
93
return dict (list (self .internal_plugin_metadata .items ()) + list (self .external_plugin_metadata .items ()))
93
94
94
- def find_internal_plugins (self ):
95
- """Populates self.internal_plugin_metadata """
96
- iter_modules = list (pkgutil .iter_modules ([self . pkgpath ]))
95
+ def find_directory_plugins (self , pkg_path : str , external : bool ):
96
+ """Finds plugins in directory form from the given pkg_path and populates the metadata dicts """
97
+ iter_modules = list (pkgutil .iter_modules ([pkg_path ]))
97
98
for loader , name , ispkg in iter_modules :
98
99
# FIXME pyinstaller binaries are packaging each built-in plugin twice:
99
100
# once as data and once as code. To honor the "no duplicates" rule below,
@@ -102,8 +103,19 @@ def find_internal_plugins(self):
102
103
continue
103
104
if self .cmd_only and self .config .get ('enable_plugin_' + name ) is not True :
104
105
continue
105
- full_name = f'electrum.plugins.{ name } ' + ('.commands' if self .cmd_only else '' )
106
- spec = importlib .util .find_spec (full_name )
106
+ base_name = 'electrum.plugins' if not external else 'electrum_external_plugins'
107
+ full_name = f'{ base_name } .{ name } ' + ('.commands' if self .cmd_only else '' )
108
+ if external :
109
+ module_path = os .path .join (pkg_path , name )
110
+ if not self ._has_recursive_root_permissions (module_path ):
111
+ self .logger .info (f"Not loading plugin { module_path } : directory has user write permissions" )
112
+ continue
113
+ module_path = os .path .join (module_path , 'commands.py' if self .cmd_only else '__init__.py' )
114
+ if not os .path .exists (module_path ):
115
+ continue
116
+ spec = importlib .util .spec_from_file_location (full_name , module_path )
117
+ else :
118
+ spec = importlib .util .find_spec (full_name )
107
119
if spec is None :
108
120
if self .cmd_only :
109
121
continue # no commands module in this plugin
@@ -129,10 +141,13 @@ def find_internal_plugins(self):
129
141
if d .get ('requires_wallet_type' ):
130
142
# trustedcoin will not be added to list
131
143
continue
132
- if name in self .internal_plugin_metadata :
144
+ if name in self .internal_plugin_metadata or name in self . external_plugin_metadata :
133
145
_logger .info (f"Found the following plugin modules: { iter_modules = } " )
134
146
raise Exception (f"duplicate plugins? for { name = } " )
135
- self .internal_plugin_metadata [name ] = d
147
+ if not external :
148
+ self .internal_plugin_metadata [name ] = d
149
+ else :
150
+ self .external_plugin_metadata [name ] = d
136
151
137
152
@staticmethod
138
153
def exec_module_from_spec (spec , path ):
@@ -147,44 +162,36 @@ def exec_module_from_spec(spec, path):
147
162
return module
148
163
149
164
def find_plugins (self ):
150
- self .find_internal_plugins ()
151
- self .find_external_plugins ()
165
+ internal_plugins_path = (self .pkgpath , False )
166
+ external_plugins_path = (self .get_external_plugin_dir (), True )
167
+ for pkg_path , external in (internal_plugins_path , external_plugins_path ):
168
+ # external plugins enforce root permissions on the directory
169
+ if pkg_path and os .path .exists (pkg_path ):
170
+ self .find_directory_plugins (pkg_path = pkg_path , external = external )
171
+ self .find_zip_plugins (pkg_path = pkg_path , external = external )
152
172
153
173
def load_plugins (self ):
154
- self .load_internal_plugins ()
155
- self .load_external_plugins ()
156
-
157
- def load_internal_plugins (self ):
158
- for name , d in self .internal_plugin_metadata .items ():
174
+ for name , d in chain (self .internal_plugin_metadata .items (), self .external_plugin_metadata .items ()):
159
175
if not d .get ('requires_wallet_type' ) and self .config .get ('enable_plugin_' + name ):
160
176
try :
161
- self .load_internal_plugin (name )
177
+ self .load_plugin_by_name (name )
162
178
except BaseException as e :
163
179
self .logger .exception (f"cannot initialize plugin { name } : { e } " )
164
180
165
- def load_external_plugin (self , name ):
166
- if name in self .plugins :
167
- return self .plugins [name ]
168
- # If we do not have the metadata, it was not detected by `load_external_plugins`
169
- # on startup, or added by manual user installation after that point.
170
- metadata = self .external_plugin_metadata .get (name )
171
- if metadata is None :
172
- self .logger .exception (f"attempted to load unknown external plugin { name } " )
173
- return
174
- full_name = f'electrum_external_plugins.{ name } .{ self .gui_name } '
175
- spec = importlib .util .find_spec (full_name )
176
- if spec is None :
177
- raise RuntimeError (f"{ self .gui_name } implementation for { name } plugin not found" )
178
- module = self .exec_module_from_spec (spec , full_name )
179
- plugin = module .Plugin (self , self .config , name )
180
- self .add_jobs (plugin .thread_jobs ())
181
- self .plugins [name ] = plugin
182
- self .logger .info (f"loaded external plugin { name } " )
183
- return plugin
184
-
185
181
def _has_root_permissions (self , path ):
186
182
return os .stat (path ).st_uid == 0 and not os .access (path , os .W_OK )
187
183
184
+ @profiler (min_threshold = 0.5 )
185
+ def _has_recursive_root_permissions (self , path ):
186
+ """Check if a directory and all its subdirectories have root permissions"""
187
+ for root , dirs , files in os .walk (path ):
188
+ if not self ._has_root_permissions (root ):
189
+ return False
190
+ for f in files :
191
+ if not self ._has_root_permissions (os .path .join (root , f )):
192
+ return False
193
+ return True
194
+
188
195
def get_external_plugin_dir (self ):
189
196
if sys .platform not in ['linux' , 'darwin' ] and not sys .platform .startswith ('freebsd' ):
190
197
return
@@ -197,18 +204,23 @@ def get_external_plugin_dir(self):
197
204
return
198
205
return pkg_path
199
206
200
- def external_plugin_path (self , name ):
201
- metadata = self .external_plugin_metadata [name ]
202
- filename = metadata ['filename' ]
203
- return os .path .join (self .get_external_plugin_dir (), filename )
207
+ def zip_plugin_path (self , name ):
208
+ filename = self .get_metadata (name )['filename' ]
209
+ if name in self .internal_plugin_metadata :
210
+ pkg_path = self .pkgpath
211
+ else :
212
+ pkg_path = self .get_external_plugin_dir ()
213
+ return os .path .join (pkg_path , filename )
204
214
205
- def find_external_plugins (self ):
206
- pkg_path = self . get_external_plugin_dir ()
215
+ def find_zip_plugins (self , pkg_path : str , external : bool ):
216
+ """Finds plugins in zip form in the given pkg_path and populates the metadata dicts"""
207
217
if pkg_path is None :
208
218
return
209
219
for filename in os .listdir (pkg_path ):
210
220
path = os .path .join (pkg_path , filename )
211
- if not self ._has_root_permissions (path ):
221
+ if not filename .endswith ('.zip' ):
222
+ continue
223
+ if external and not self ._has_root_permissions (path ):
212
224
self .logger .info (f'not loading { path } : file has user write permissions' )
213
225
continue
214
226
try :
@@ -223,7 +235,7 @@ def find_external_plugins(self):
223
235
raise Exception (f"duplicate plugins for name={ name } " )
224
236
if name in self .external_plugin_metadata :
225
237
raise Exception (f"duplicate plugins for name={ name } " )
226
- module_path = f'electrum_external_plugins.{ name } '
238
+ module_path = f'electrum_external_plugins.{ name } ' if external else f'electrum.plugins. { name } '
227
239
spec = zipfile .find_spec (name )
228
240
module = self .exec_module_from_spec (spec , module_path )
229
241
if self .cmd_only :
@@ -245,16 +257,11 @@ def find_external_plugins(self):
245
257
continue
246
258
d ['display_name' ] = d ['fullname' ]
247
259
d ['zip_hash_sha256' ] = get_file_hash256 (path )
248
- self .external_plugin_metadata [name ] = d
249
-
250
- def load_external_plugins (self ):
251
- for name , d in self .external_plugin_metadata .items ():
252
- if self .config .get ('enable_plugin_' + name ):
253
- try :
254
- self .load_external_plugin (name )
255
- except BaseException as e :
256
- traceback .print_exc (file = sys .stdout ) # shouldn't this be... suppressed unless -v?
257
- self .logger .exception (f"cannot initialize plugin { name } { e !r} " )
260
+ d ['is_zip' ] = True
261
+ if external :
262
+ self .external_plugin_metadata [name ] = d
263
+ else :
264
+ self .internal_plugin_metadata [name ] = d
258
265
259
266
def get (self , name ):
260
267
return self .plugins .get (name )
@@ -266,23 +273,31 @@ def load_plugin(self, name) -> 'BasePlugin':
266
273
"""Imports the code of the given plugin.
267
274
note: can be called from any thread.
268
275
"""
269
- if name in self .internal_plugin_metadata :
270
- return self .load_internal_plugin (name )
271
- elif name in self .external_plugin_metadata :
272
- return self .load_external_plugin (name )
276
+ if self .get_metadata (name ):
277
+ return self .load_plugin_by_name (name )
273
278
else :
274
279
raise Exception (f"could not find plugin { name !r} " )
275
280
276
- def load_internal_plugin (self , name ) -> 'BasePlugin' :
281
+ def load_plugin_by_name (self , name ) -> 'BasePlugin' :
277
282
if name in self .plugins :
278
283
return self .plugins [name ]
279
- full_name = f'electrum.plugins.{ name } .{ self .gui_name } '
284
+
285
+ is_zip = self .is_plugin_zip (name )
286
+ is_external = name in self .external_plugin_metadata
287
+ if not is_external :
288
+ full_name = f'electrum.plugins.{ name } .{ self .gui_name } '
289
+ else :
290
+ full_name = f'electrum_external_plugins.{ name } .{ self .gui_name } '
291
+
280
292
spec = importlib .util .find_spec (full_name )
281
293
if spec is None :
282
294
raise RuntimeError (f"{ self .gui_name } implementation for { name } plugin not found" )
283
295
try :
284
- module = importlib .util .module_from_spec (spec )
285
- spec .loader .exec_module (module )
296
+ if is_zip :
297
+ module = self .exec_module_from_spec (spec , full_name )
298
+ else :
299
+ module = importlib .util .module_from_spec (spec )
300
+ spec .loader .exec_module (module )
286
301
plugin = module .Plugin (self , self .config , name )
287
302
except Exception as e :
288
303
raise Exception (f"Error loading { name } plugin: { repr (e )} " ) from e
@@ -376,6 +391,19 @@ def get_plugin(self, name: str) -> 'BasePlugin':
376
391
self .load_plugin (name )
377
392
return self .plugins [name ]
378
393
394
+ def is_plugin_zip (self , name : str ) -> bool :
395
+ """Returns True if the plugin is a zip file"""
396
+ if (metadata := self .get_metadata (name )) is None :
397
+ return False
398
+ return metadata .get ('is_zip' , False )
399
+
400
+ def get_metadata (self , name : str ) -> Optional [dict ]:
401
+ """Returns the metadata of the plugin"""
402
+ metadata = self .internal_plugin_metadata .get (name ) or self .external_plugin_metadata .get (name )
403
+ if not metadata :
404
+ return None
405
+ return metadata
406
+
379
407
def run (self ):
380
408
while self .is_running ():
381
409
self .wake_up_event .wait (0.1 ) # time.sleep(0.1) OR event
@@ -470,13 +498,16 @@ def settings_dialog(self, window):
470
498
471
499
def read_file (self , filename : str ) -> bytes :
472
500
import zipfile
473
- if self .name in self . parent .external_plugin_metadata :
474
- plugin_filename = self .parent .external_plugin_path (self .name )
501
+ if self .parent .is_plugin_zip ( self . name ) :
502
+ plugin_filename = self .parent .zip_plugin_path (self .name )
475
503
with zipfile .ZipFile (plugin_filename ) as myzip :
476
504
with myzip .open (os .path .join (self .name , filename )) as myfile :
477
505
return myfile .read ()
478
506
else :
479
- path = os .path .join (os .path .dirname (__file__ ), 'plugins' , self .name , filename )
507
+ if filename in self .parent .internal_plugin_metadata :
508
+ path = os .path .join (os .path .dirname (__file__ ), 'plugins' , self .name , filename )
509
+ else :
510
+ path = os .path .join (self .parent .get_external_plugin_dir (), self .name , filename )
480
511
with open (path , 'rb' ) as myfile :
481
512
return myfile .read ()
482
513
0 commit comments