pluginsort: add some debug code to pluginsort regarding shutdown problems
[enigma2-plugins.git] / pluginsort / src / plugin.py
1 from . import _
2
3 # Plugin definition
4 from Plugins.Plugin import PluginDescriptor
5
6 from Components.config import config, ConfigSubsection, ConfigSet
7 from Screens import PluginBrowser
8 from Screens.MessageBox import MessageBox
9 from Screens.ChoiceBox import ChoiceBox
10 from Components.PluginComponent import PluginComponent, plugins
11 from Components.PluginList import PluginEntryComponent
12 from Tools.Directories import resolveFilename, fileExists, SCOPE_SKIN_IMAGE, SCOPE_PLUGINS
13 from Tools.BoundFunction import boundFunction
14 from Screens.InfoBarGenerics import InfoBarPlugins
15
16 from Components.ActionMap import ActionMap, NumberActionMap
17 from operator import attrgetter # python 2.5+
18
19 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest
20
21 from enigma import eListboxPythonMultiContent, gFont
22 from Tools.LoadPixmap import LoadPixmap
23
24 from xml.etree.cElementTree import parse as cet_parse
25 try:
26         from xml.etree.cElementTree import ParseError
27 except ImportError, ie:
28         ParseError = SyntaxError
29
30 from shutil import copyfile, Error
31
32 XML_CONFIG = "/etc/enigma2/pluginsort.xml"
33
34 def SelectedPluginEntryComponent(plugin):
35         if plugin.icon is None:
36                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "skin_default/icons/plugin.png"))
37         else:
38                 png = plugin.icon
39
40         return [
41                 plugin,
42                 MultiContentEntryText(pos=(0,0), size=(440, 50), backcolor_sel=8388608),
43                 MultiContentEntryText(pos=(120, 5), size=(320, 25), font=0, text=plugin.name),
44                 MultiContentEntryText(pos=(120, 26), size=(320, 17), font=1, text=plugin.description),
45                 MultiContentEntryPixmapAlphaTest(pos=(10, 5), size=(100, 40), png = png),
46         ]
47
48 WHEREMAP = {}
49 pdict = PluginDescriptor.__dict__
50 for where in pdict:
51         if where.startswith('WHERE_'):
52                 WHEREMAP[where] = pdict[where]
53 del pdict
54 reverse = lambda map: dict(zip(map.values(), map.keys()))
55
56 class PluginWeights:
57         def __init__(self):
58                 self.plugins = {}
59                 self.load()
60
61         def load(self):
62                 if not fileExists(XML_CONFIG):
63                         return
64
65                 try:
66                         config = cet_parse(XML_CONFIG).getroot()
67                 except ParseError, pe:
68                         from time import time
69                         print "[PluginSort] Parse Error occured in configuration, backing it up and starting from scratch!"
70                         try:
71                                 copyfile(XML_CONFIG, "/etc/enigma2/pluginsort.xml.%d" % (int(time()),))
72                         except Error, she:
73                                 print "[PluginSort] Uh oh, failed to create the backup... I hope you have one anyway :D"
74                         return
75
76                 for wheresection in config.findall('where'):
77                         where = wheresection.get('type')
78                         whereid = WHEREMAP.get(where, None)
79                         whereplugins = wheresection.findall('plugin')
80                         if whereid is None or not whereplugins:
81                                 print "[PluginSort] Ignoring section %s because of invalid id (%s) or no plugins (%s)" % (where, repr(whereid), repr(whereplugins))
82                                 continue
83
84                         for plugin in whereplugins:
85                                 name = plugin.get('name')
86                                 try:
87                                         weight = int(plugin.get('weight'))
88                                 except ValueError, ve:
89                                         print "[PluginSort] Invalid weight of %s received for plugin %s, ignoring" % (repr(plugin.get('weight')), repr(name))
90                                 else:
91                                         self.plugins.setdefault(whereid, {})[name] = weight
92
93         def save(self):
94                 list = ['<?xml version="1.0" ?>\n<pluginsort>\n\n']
95                 append = list.append
96                 extend = list.extend
97
98                 idmap = reverse(WHEREMAP)
99                 for key in self.plugins.keys():
100                         whereplugins = self.plugins.get(key, None)
101                         if not whereplugins:
102                                 continue
103
104                         where = idmap[key]
105                         extend((' <where type="', str(where), '">\n'))
106                         for key, value in whereplugins.iteritems():
107                                 extend(('  <plugin name="', str(key), '" weight="', str(value), '" />\n'))
108                         append((' </where>\n'))
109                 append('\n</pluginsort>\n')
110                 
111                 file = open(XML_CONFIG, 'w')
112                 file.writelines(list)
113                 file.close()
114
115         def get(self, plugin):
116                 for x in plugin.where:
117                         whereplugins = self.plugins.get(x, None)
118                         weight = whereplugins and whereplugins.get(plugin.name, None)
119                         if weight is not None:
120                                 return weight
121                 return plugin.weight
122
123         def set(self, plugin):
124                 for x in plugin.where:
125                         whereplugins = self.plugins.get(x, None)
126                         if whereplugins:
127                                 whereplugins[plugin.name] = plugin.weight
128                         else:
129                                 self.plugins[x] = {plugin.name: plugin.weight}
130
131 pluginWeights = PluginWeights()
132
133 def PluginComponent_addPlugin(self, plugin, *args, **kwargs):
134         if len(plugin.where) > 1:
135                 print "[PluginSort] Splitting %s up in individual entries (%s)" % (plugin.name, repr(plugin.where))
136                 if hasattr(plugin, 'iconstr'):
137                         icon = plugin.iconstr
138                 else:
139                         icon = plugin.icon
140
141                 for x in plugin.where:
142                         pd = PluginDescriptor(name=plugin.name, where=[x], description=plugin.description, icon=icon, fnc=plugin.__call__, wakeupfnc=plugin.wakeupfnc, needsRestart=plugin.needsRestart, internal=plugin.internal, weight=plugin.weight)
143
144                         newWeight = pluginWeights.get(plugin)
145                         print "[PluginSort] Setting weight of %s from %d to %d" % (plugin.name, plugin.weight, newWeight)
146                         pd.weight = newWeight
147                         PluginComponent.pluginSort_baseAddPlugin(self, pd, *args, **kwargs)
148
149                 # installedPluginList is a list of original descriptors, but we changed it to be a copy, not a reference. so keep it up to date
150                 if self.firstRun:
151                         self.installedPluginList.append(plugin)
152                         print "[PluginSort] Adding %s to list of installed plugins (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where))
153                 return
154
155         newWeight = pluginWeights.get(plugin)
156         print "[PluginSort] Setting weight of %s from %d to %d" % (plugin.name, plugin.weight, newWeight)
157         plugin.weight = newWeight
158         PluginComponent.pluginSort_baseAddPlugin(self, plugin, *args, **kwargs)
159
160         if self.firstRun:
161                 self.installedPluginList.append(plugin)
162
163 def PluginComponent_removePlugin(self, plugin, *args, **kwargs):
164         print "[PluginSort] Supposed to remove plugin: %s (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where))
165         try:
166                 PluginComponent.pluginSort_baseRemovePlugin(self, plugin, *args, **kwargs)
167         except ValueError, ve:
168                 print "[PluginSort] Ignoring failure to remove plugin %s, please DO NOT IGNORE THIS!" % (plugin.name,)
169                 import traceback, sys
170                 traceback.print_exc(file=sys.stdout)
171                 print "[PluginSort] pluginList: %s" % (repr(self.pluginList),)
172                 print "[PluginSort] plugins: %s" % (repr(self.plugins),)
173
174 OriginalPluginBrowser = PluginBrowser.PluginBrowser
175 class SortingPluginBrowser(OriginalPluginBrowser):
176         def __init__(self, *args, **kwargs):
177                 self.movemode = False
178                 self.selected = -1
179                 if 'where' in kwargs:
180                         self.where = kwargs['where']
181                         del kwargs['where']
182                 else:
183                         self.where = PluginDescriptor.WHERE_PLUGINMENU
184
185                 OriginalPluginBrowser.__init__(self, *args, **kwargs)
186                 self.skinName = ["SortingPluginBrowser", "PluginBrowser"]
187
188                 self["ColorActions"] = ActionMap(["ColorActions"],
189                         {
190                                 "green": self.toggleMoveMode,
191                         }, -2
192                 )
193                 self["ColorActions"].setEnabled(False)
194
195                 self["WizardActions"] = ActionMap(["WizardActions"],
196                         {
197                                 "left": boundFunction(self.doMove, self["list"].pageUp),
198                                 "right": boundFunction(self.doMove, self["list"].pageDown),
199                                 "up": boundFunction(self.doMove, self["list"].up),
200                                 "down": boundFunction(self.doMove, self["list"].down),
201                         }, -2
202                 )
203
204                 if self.where != PluginDescriptor.WHERE_PLUGINMENU:
205                         self.toggleMoveMode()
206                         self.onShow.append(self.setCustomTitle)
207                 else:
208                         self["NumberActions"] = NumberActionMap(["NumberActions"],
209                                 {
210                                         "1": self.keyNumberGlobal,
211                                         "2": self.keyNumberGlobal,
212                                         "3": self.keyNumberGlobal,
213                                         "4": self.keyNumberGlobal,
214                                         "5": self.keyNumberGlobal,
215                                         "6": self.keyNumberGlobal,
216                                         "7": self.keyNumberGlobal,
217                                         "8": self.keyNumberGlobal,
218                                         "9": self.keyNumberGlobal,
219                                         "0": self.keyNumberGlobal,
220                                 }, -2
221                         )
222
223                         self["MenuActions"] = ActionMap(["MenuActions"],
224                                 {
225                                         "menu": self.openMenu,
226                                 }, -1
227                         )
228
229         def setCustomTitle(self):
230                 titleMap = {
231                         PluginDescriptor.WHERE_EXTENSIONSMENU: _("Sort Extensions"),
232                         PluginDescriptor.WHERE_MOVIELIST: _("Sort MovieList Extensions"),
233                         PluginDescriptor.WHERE_EVENTINFO: _("Sort EventInfo Extensions"),
234                 }
235                 title = titleMap.get(self.where, None)
236                 if title:
237                         self.setTitle(title)
238
239         def keyNumberGlobal(self, number):
240                 if not self.movemode:
241                         realnumber = (number - 1) % 10
242                         if realnumber < len(self.list):
243                                 self["list"].moveToIndex(realnumber)
244                                 self.save()
245
246         def close(self, *args, **kwargs):
247                 if self.movemode:
248                         self.toggleMoveMode()
249                 OriginalPluginBrowser.close(self, *args, **kwargs)
250
251         # copied from PluginBrowser because we redo pretty much anything :-)
252         def updateList(self):
253                 self.pluginlist = plugins.getPlugins(self.where)
254                 if self.where in (PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU):
255                         self.pluginlist.sort(key=attrgetter('weight', 'name')) # sort first by weight, then by name; we get pretty much a weight sorted but otherwise random list
256                 else: #if self.where in (PluginDescriptor.WHERE_EVENTINFO, PluginDescriptor.WHERE_MOVIELIST):
257                         self.pluginlist.sort(key=attrgetter('weight'))
258                 self.list = [PluginEntryComponent(plugin) for plugin in self.pluginlist]
259                 self["list"].l.setList(self.list)
260                 if self.where == PluginDescriptor.WHERE_PLUGINMENU:
261                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
262                                 # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
263                                 self["red"].setText(_("Manage extensions"))
264                                 self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
265                                 self["SoftwareActions"].setEnabled(True)
266                                 self["PluginDownloadActions"].setEnabled(False)
267                                 self["ColorActions"].setEnabled(True)
268                         else:
269                                 # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
270                                 self["red"].setText(_("Remove Plugins"))
271                                 # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
272                                 self["green"].setText(_("Download Plugins"))
273                                 self["SoftwareActions"].setEnabled(False)
274                                 self["PluginDownloadActions"].setEnabled(True)
275                                 self["ColorActions"].setEnabled(False)
276                 else:
277                         self["red"].setText("")
278                         self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
279                         self["SoftwareActions"].setEnabled(False)
280                         self["PluginDownloadActions"].setEnabled(False)
281                         self["ColorActions"].setEnabled(True)
282
283         def doMove(self, func):
284                 if self.selected != -1:
285                         oldpos = self["list"].getSelectedIndex()
286                         func()
287                         entry = self.list.pop(oldpos)
288                         newpos = self["list"].getSelectedIndex()
289                         self.list.insert(newpos, entry)
290                         self["list"].l.setList(self.list)
291                 else:
292                         func()
293
294         def save(self):
295                 selected = self.selected
296                 if not self.movemode:
297                         OriginalPluginBrowser.save(self)
298                 elif selected != -1:
299                         Len = len(self.pluginlist)
300                         newpos = self["list"].getSelectedIndex()
301                         entry = self.pluginlist[selected]
302                         self.pluginlist.remove(entry)
303                         self.pluginlist.insert(newpos, entry)
304
305                         # we moved up, increase weight of plugins after us
306                         if newpos < selected:
307                                 print "[PluginSort]", entry.name, "moved up"
308                                 i = newpos + 1
309                                 # since we moved up, there has to be an entry after this one
310                                 diff = abs(self.pluginlist[i].weight - self.pluginlist[newpos].weight) + 1
311                                 print "[PluginSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (i, self.pluginlist[i].weight, newpos, self.pluginlist[newpos].weight, diff)
312                                 while i < Len:
313                                         print "[PluginSort] INCREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff
314                                         self.pluginlist[i].weight += diff
315                                         i += 1
316                         # we moved down, decrease weight of plugins before us
317                         elif newpos > selected:
318                                 print "[PluginSort]", entry.name, "moved down"
319                                 i = newpos - 1
320                                 # since we moved up, there has to be an entry before this one
321                                 diff = abs(self.pluginlist[newpos].weight - self.pluginlist[i].weight) + 1
322                                 print "[PluginSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (newpos, self.pluginlist[newpos].weight, i, self.pluginlist[i].weight, diff)
323                                 while i > -1:
324                                         print "[PluginSort] DECREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff
325                                         self.pluginlist[i].weight -= diff
326                                         i -= 1
327                         else:
328                                 print "[PluginSort]", entry.name, "did not move (%d to %d)?" % (selected, newpos)
329
330                         self.list = [PluginEntryComponent(plugin) for plugin in self.pluginlist]
331                         print "[PluginSort] NEW LIST:", [(plugin.name, plugin.weight) for plugin in self.pluginlist]
332                         self["list"].l.setList(self.list)
333                         self.selected = -1
334                 else:
335                         self.selected = self["list"].getSelectedIndex()
336                         self.list[self.selected] = SelectedPluginEntryComponent(self.pluginlist[self.selected])
337                         self["list"].l.setList(self.list)
338         
339         def openMenu(self):
340                 if self.movemode:
341                         # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
342                         moveString = _("disable move mode")
343                 else:
344                         # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
345                         moveString = _("enable move mode")
346
347                 list = [
348                         (moveString, self.toggleMoveMode),
349                         (_("move extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_EXTENSIONSMENU)),
350                         (_("move movie extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_MOVIELIST)),
351                         (_("move event extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_EVENTINFO)),
352                 ]
353
354                 if fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/PluginHider/plugin.py")):
355                         list.insert(0, (_("hide selected plugin"), self.hidePlugin))
356
357                 self.session.openWithCallback(
358                         self.menuCallback,
359                         ChoiceBox,
360                         list = list,
361                 )
362
363         def menuCallback(self, ret):
364                 ret and ret[1]()
365
366         def openMover(self, where):
367                 self.session.open(SortingPluginBrowser, where=where)
368
369         def hidePlugin(self):
370                 try:
371                         from Plugins.Extensions.PluginHider.plugin import hidePlugin
372                 except Exception, e:
373                         self.session.open(MessageBox, _("Unable to load PluginHider"), MessageBox.TYPE_ERROR)
374                 else:
375                         hidePlugin(self["list"].l.getCurrentSelection()[0])
376
377                         # we were actually in move mode, so save the current position
378                         if self.selected != -1:
379                                 self.save()
380                         self.updateList()
381
382         def toggleMoveMode(self):
383                 if self.movemode:
384                         if self.selected != -1:
385                                 self.save()
386                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
387                                 self["green"].setText(_("Sort"))
388
389                         for plugin in self.pluginlist:
390                                 pluginWeights.set(plugin)
391                         pluginWeights.save()
392
393                         # auto-close if not "PluginBrowser"
394                         if self.where != PluginDescriptor.WHERE_PLUGINMENU:
395                                 self.movemode = False
396                                 return self.close()
397                 else:
398                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
399                                 self["green"].setText(_("End Sort"))
400                 self.movemode = not self.movemode
401
402 def autostart(reason, *args, **kwargs):
403         if reason == 0:
404                 if hasattr(PluginComponent, 'pluginSort_baseAddPlugin'):
405                         print "[PluginSort] Something went wrong as our autostart handler was called multiple times for startup, printing traceback and ignoring."
406                         import traceback, sys
407                         traceback.print_stack(limit=5, file=sys.stdout)
408                 else:
409                         PluginComponent.pluginSort_baseAddPlugin = PluginComponent.addPlugin
410                         PluginComponent.addPlugin = PluginComponent_addPlugin
411
412                         PluginComponent.pluginSort_baseRemovePlugin = PluginComponent.removePlugin
413                         PluginComponent.removePlugin = PluginComponent_removePlugin
414
415                         # we use a copy for installed plugins because we might change the 'where'-lists
416                         plugins.installedPluginList = plugins.pluginList[:]
417                         def PluginComponent__setattr__(self, key, value):
418                                 if key == 'installedPluginList': return
419                                 else: self.__dict__[key] = value
420                         PluginComponent.__setattr__ = PluginComponent__setattr__
421
422                         # "fix" weight of plugins already added to list, future ones will be fixed automatically
423                         for plugin in plugins.getPlugins([PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU, PluginDescriptor.WHERE_MOVIELIST, PluginDescriptor.WHERE_EVENTINFO]):
424                                 # create individual entries for multiple wheres, this is potentially harmful!
425                                 if len(plugin.where) > 1:
426                                         # remove all entries except for a potential autostart one (highly unlikely to mix autostart with one of the above, but you never know :D)
427                                         if PluginDescriptor.WHERE_AUTOSTART in plugin.where:
428                                                 plugin.where.remove(PluginDescriptor.WHERE_AUTOSTART)
429                                                 hadAutostart = True
430                                         else:
431                                                 hadAutostart = False
432                                         plugins.removePlugin(plugin)
433                                         plugins.addPlugin(plugin) # this is our own addPlugin now, which automatically creates copies
434
435                                         # HACK: re-add autostart entry to internal list inside PluginComponent
436                                         if hadAutostart:
437                                                 plugin.where = [ PluginDescriptor.WHERE_AUTOSTART ]
438                                                 plugins.pluginList.append(plugin)
439
440                                 # we're keeping the entry, just fix the weight
441                                 else:
442                                         newWeight = pluginWeights.get(plugin)
443                                         print "[PluginSort] Fixing weight for %s (was %d, now %d)" % (plugin.name, plugin.weight, newWeight)
444                                         plugin.weight = newWeight
445
446                         # let movieepg fix extensions list sorting if installed, else do this ourselves
447                         if not fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/MovieEPG/plugin.py")):
448                                 def InfoBarPlugins_getPluginList(self, *args, **kwargs):
449                                         l = InfoBarPlugins.pluginSort_baseGetPluginList(self, *args, **kwargs)
450                                         try:
451                                                 l.sort(key=lambda e: (e[0][1].args[0].weight, e[2]))
452                                         except Exception, e:
453                                                 print "[PluginSort] Failed to sort extensions", e
454                                         return l
455
456                                 InfoBarPlugins.pluginSort_baseGetPluginList = InfoBarPlugins.getPluginList
457                                 InfoBarPlugins.getPluginList = InfoBarPlugins_getPluginList
458
459
460                         PluginBrowser.PluginBrowser = SortingPluginBrowser
461         else:
462                 if hasattr(PluginComponent, 'pluginSort_baseAddPlugin'):
463                         PluginComponent.addPlugin = PluginComponent.pluginSort_baseAddPlugin
464                         del PluginComponent.pluginSort_baseAddPlugin
465                         PluginComponent.removePlugin = PluginComponent.pluginSort_baseRemovePlugin
466                         del PluginComponent.pluginSort_baseRemovePlugin
467                 if hasattr(InfoBarPlugins, 'pluginSort_baseGetPluginList'):
468                         InfoBarPlugins.getPluginList = InfoBarPlugins.pluginSort_baseGetPluginList
469                         del InfoBarPlugins.pluginSort_baseGetPluginList
470                 PluginBrowser.PluginBrowser = OriginalPluginBrowser
471
472 def Plugins(**kwargs):
473         return [
474                 PluginDescriptor(
475                         where=PluginDescriptor.WHERE_AUTOSTART,
476                         fnc=autostart,
477                         needsRestart=True,
478                 ),
479         ]