source: lliurex-store/trunk/fuentes/python3-lliurex-store.install/usr/share/lliurexstore/storeManager.py @ 6458

Last change on this file since 6458 was 6458, checked in by Juanma, 3 years ago

add python3-bs4 dependency

File size: 20.2 KB
Line 
1#!/usr/bin/python3
2import sys
3import os
4import threading
5import syslog
6import pkgutil
7import lliurexstore.plugins as plugins
8import json
9######
10#Ver. 1.0 of storeManager.py
11# This class manages the store and the related plugins
12# It's implemented as an action-drived class.
13# There're four(five) main actions and each of them could execute and undeterminated number of subprocess in their respective thread
14# Each of these actions returns EVER a list of dictionaries.
15#####
16
17class StoreManager:
18        def __init__(self,args=()):
19                #               self.store=appstream.Store()
20                self.store=None
21                self.relatedActions={
22                                        'load':['load'],
23                                        'search':['search','get_info','pkginfo'],
24                                        'list':['list','get_info','pkginfo'],
25                                        'info':['list','get_info','pkginfo'],
26                                        'list_sections':['list_sections'],
27                                        'install':['search','get_info','pkginfo','install'],
28                                        'remove':['search','get_info','pkginfo','remove']
29                                        }
30                self.loadBundles=False
31                self.dbg=0
32                self._propagate_dbg=0
33                if args:
34                        if args[0]==1:
35                                self.loadBundles=True
36                        if args[1]==1:
37                                self.dbg=args
38                self.threads={}                         #Dict that stores the functions that must execute each action
39                self.threadsProgress={}                 #"" "" "" the progress for each launched thread
40                self.threadsRunning={}                  #"" "" "" the running threads
41                self.registeredPlugins={}               #Dict that have the relation between plugins and actions
42                self.registerProcessProgress={}         #Dict that stores the progress for each function/parentAction pair
43                self.progressActions={}                 #Progress of global actions based on average progress of individual processes
44                self.extraActions={}            #Dict that have the actions managed by plugins and no defined on the main class as relatedActions
45                self.result={}                          #Result of the actions
46                self.lock=threading.Lock()              #locker for functions related to threads (get_progress, is_action_running...)
47                self.main()
48        #def __init__
49
50        def main(self):
51                self._define_functions_for_threads()    #Function that loads the dictionary self.threads
52                self.__init_plugins__()                 #Function that loads the plugins
53                self.execute_action('load')             #Initial load of the store
54        #def main
55
56        ####
57        #Load and register the plugins from plugin dir
58        ####
59        def __init_plugins__(self):
60                package=plugins
61                for importer, mod, ispkg in pkgutil.walk_packages(path=package.__path__, prefix=package.__name__+'.',onerror=lambda x: None):
62                        strImport='from '+mod+' import *'
63                        try:
64                                self._debug("Importing "+str(mod))
65                                exec (strImport)
66                        except Exception as e:
67                                print(str(e))
68
69                for mod in (sys.modules.keys()):
70                        if 'plugins.' in mod:
71                                definedActions={}
72                                pluginNameUp=mod.split('.')[-1]
73                                pluginName=pluginNameUp.lower()
74                                self._debug("Initializing "+str(pluginName))
75                                try:
76                                        loadedClass=eval(pluginName)()
77                                        definedActions=loadedClass.register()
78                                except Exception as e:
79                                        print ("Can't initialize "+str(mod)+' '+str(loadedClass))
80                                        print ("Reason: "+str(e))
81                                        pass
82                       
83                                for action in definedActions.keys():
84                                        if action not in self.registeredPlugins:
85                                                self.registeredPlugins[action]={}
86                                        self.registeredPlugins[action].update({definedActions[action]:'plugins.'+pluginNameUp+'.'+pluginName})
87                self._debug(str(self.registeredPlugins))
88        #def __init_plugins__
89
90        def set_debug(self,dbg='1'):
91                self.dbg=int(dbg)
92                self._debug ("Debug enabled")
93        #def set_debug
94
95        def _debug(self,msg=''):
96                if self.dbg==1:
97                        print ('DEBUG Store: '+msg)
98        #def _debug
99
100        def _log(self,msg=None):
101                if msg:
102                        syslog.openlog('lliurex-store')
103                        syslog.syslog(msg)
104                        self._debug(msg)
105        ####
106        #dict of actions/related functions for threading
107        ####
108        def _define_functions_for_threads(self):
109                self.threads['load']="threading.Thread(target=self._load_Store)"
110                self.threads['get_info']="threading.Thread(target=self._get_App_Info,args=[args])"
111                self.threads['pkginfo']="threading.Thread(target=self._get_Extended_App_Info,args=[args])"
112                self.threads['search']='threading.Thread(target=self._search_Store,args=[args,action])'
113                self.threads['list']='threading.Thread(target=self._search_Store,args=[args,action])'
114                self.threads['info']='threading.Thread(target=self._search_Store,args=[args,action])'
115                self.threads['install']='threading.Thread(target=self._install_remove_App,args=[args,action])'
116                self.threads['remove']='threading.Thread(target=self._install_remove_App,args=[args,action])'
117                self.threads['list_sections']='threading.Thread(target=self._list_sections,args=[args,action])'
118        #def _define_functions_for_threads
119
120        ####
121        #Launch the appropiate threaded function for the desired action
122        #Input:
123        #  - action to be executed
124        #  - parms for the action
125        ####
126        def execute_action(self,action,args=None):
127                self._debug("Launching action: "+action+" and args "+str(args))
128                if self.is_action_running('load'):
129                        self._join_action('load')
130                sw_withoutStatus=False
131                if action not in self.threads.keys():
132                        #Attempt to add a new action managed by a plugin
133                        self._debug("Attempting to find a plugin for action "+action)
134                        if action in self.registeredPlugins.keys():
135                                for packageType,plugin in self.registeredPlugins[action].items():
136                                        self.progressActions[action]=0
137                                        self.threads[action]='threading.Thread(target=self._execute_class_method(action,packageType,action).execute_action,args=[action])'
138                                        break
139                                self._debug('Plugin for '+action+' found: '+str(self.registeredPlugins[action]))
140                                self.relatedActions.update({action:[action]})
141                                sw_withoutStatus=True
142                if action in self.threads.keys():
143                        if self.is_action_running(action):
144                                #join thread if we're performing the same action
145                                self._debug("Waiting for current action "+action+" to end")
146                                self.threadsRunning[action].join()
147                        try:
148                                if action in self.progressActions.keys():
149                                        self.progressActions[action]=0
150                                self.progressActions[action]=0
151                                self.result[action]={}
152                                self.threadsRunning[action]=eval(self.threads[action])
153                                self.threadsRunning[action].start()
154                                if sw_withoutStatus:
155                                        self.result[action]['status']={'status':0,'msg':''}
156                                else:
157                                        self.result[action]['status']={'status':-1,'msg':''}
158                                self._debug("Thread "+str(self.threadsRunning[action])+" for action "+action+" launched")
159
160                        except Exception as e:
161                                self._debug("Can't launch thread for action: "+action)
162                                self._debug("Reason: "+str(e))
163                                pass
164                else:
165                        self._debug("No function associated with action "+action)
166        #def execute_action
167
168        ####
169        #Launch the appropiate class function
170        #Input:
171        #  - class action to be executed
172        #  - parms for the action
173        #  - parent action that demands the execution
174        #Output
175        #  - The class method to execute
176        ####
177        def _execute_class_method(self,action,parms=None,launchedby=None):
178                exeFunction=None
179                if not parms:
180                        parms="*"
181                if action in self.registeredPlugins:
182                        self._debug("Plugin for "+action+": "+self.registeredPlugins[action][parms])
183                        exeFunction=eval(self.registeredPlugins[action][parms]+"()")
184                        if self._propagate_dbg:
185                                exeFunction.set_debug(1)
186                        self._registerProcessProgress(action,exeFunction,launchedby)
187                else:
188                        self._debug("No plugin for action: "+action)
189
190                return (exeFunction)
191        #def _execute_class_method
192
193        ###
194        #Tell if a a action is running
195        #Input:
196        #  - action to monitorize
197        #Output:
198        #  - status true/false
199        ###
200        def is_action_running(self,actionSearched=None):
201                status=False
202                actionList=[]
203                if actionSearched:
204                        actionList.append(actionSearched)
205                else:
206                        actionList=self.relatedActions.keys()
207
208                for action in actionList:
209                        if action in self.threadsRunning.keys():
210                                if self.threadsRunning[action].is_alive():
211                                        status=True
212                                        break
213                                else:
214                                        if action in self.relatedActions.keys():
215                                                for relatedAction in self.relatedActions[action]:
216                                                        if relatedAction in self.threadsRunning.keys():
217                                                                if self.threadsRunning[relatedAction].is_alive():
218                                                                        status=True
219                                                                        break
220#                       self._debug(action+" running: "+str(status))
221                return(status)
222        #def is_action_running
223
224        ####
225        #Joins an action till finish
226        #Input:
227        #  - action to join
228        ####
229        def _join_action(self,action):
230                self._debug("Joining action: "+action)
231                try:
232                        self.threadsRunning[action].join()
233                except Exception as e:
234                        self._debug("Unable to join thread for: "+action)
235                        self._debug("Reason: "+ str(e))
236                finally:               
237                        if action in self.threadsRunning.keys():
238                                del(self.threadsRunning[action])
239        #def _join_action
240
241        ####
242        #Register the method and action/parentAction pair in the progress dict
243        #Input:
244        #  - action launched
245        #  - function (a reference to the function)
246        #  - parentAction that owns the action (if any)
247        ####
248        def _registerProcessProgress(self,action,function,parentAction=None):
249                if action in self.registerProcessProgress.keys():
250                        self._debug("Appended process for action: "+action +" and function: "+str(function))
251                        self.registerProcessProgress[action].append(function)
252                else:
253                        self._debug("Registered process for action: "+action+" and function: "+str(function))
254                        self.registerProcessProgress[action]=[function]
255                if parentAction:
256                        self._debug("Registered process for Parent Action: "+action+"-"+parentAction+" and function: "+str(function))
257                        if parentAction in self.threadsProgress.keys():
258                                self.threadsProgress[parentAction].update({action:function})
259                        else:
260                                self.threadsProgress[parentAction]={action:function}
261        #def _registerProcessProgress
262
263        ####
264        #Get the progress of the executed actions
265        #Input
266        #  - action or none if we want all of the progress
267        #Output:
268        #  - Dict of results indexed by actions
269        ####
270        def get_progress(self,action=None):
271                progress={'search':0,'list':0,'install':0,'remove':0,'load':0,'list_sections':0}
272                actionList=[]
273                if action and action in self.registerProcessProgress:
274                        actionList=[action]
275                else:
276                        actionList=self.registerProcessProgress.keys()
277                self.lock.acquire() #prevent that any thread attempts to change the iterator
278                for parentAction in self.relatedActions.keys():
279                        if self.is_action_running(parentAction):
280                                for action in actionList:
281                                        if parentAction in self.threadsProgress.keys():
282#                                               self._debug(str(len(self.threadsProgress[parentAction]))+" Threads for action "+parentAction+": "+str(self.threadsProgress[parentAction]))
283                                                acumProgress=0
284                                                for threadfunction,function in self.threadsProgress[parentAction].items():
285#                                                       function=self.threadsProgress[parentAction][threadfunction]
286#                                                       self._debug(str(function)+" "+ str(threadfunction) + " "+parentAction)
287                                                        acumProgress=acumProgress+function.progress
288#                                                       self._debug("Acum for process "+parentAction+": "+str(acumProgress))
289       
290                                                count=len(self.relatedActions[parentAction])
291#                                               self._debug("Assign result for action" +action)
292                                                self.progressActions[parentAction]=round(acumProgress/count,0)
293                                                progress[parentAction]=self.progressActions[parentAction]
294                        else:
295                                #put a 100% just in case
296                                if parentAction in self.progressActions.keys():
297#                                       if self.progressActions[parentAction]:
298#                                               self.progressActions[parentAction]=100
299                                        self.progressActions[parentAction]=100
300                self.lock.release()
301#               self._debug("Progress :"+str(progress))
302                return(self.progressActions)
303        #def get_progress
304
305        ####
306        #Gets the result of an action
307        #Input:
308        #  - action
309        #Output:
310        #  - Dict of results indexed by actions
311        ####
312        def get_result(self,action=None):
313                self.lock.acquire() #Prevent changes on results from threads
314                result={}
315                if action==None:
316                        for res in self.result.keys():
317                                if res!='load':
318                                        if 'data' in self.result[res]:
319                                                result[res]=self.result[res]['data']
320                                        else:
321                                                result[res]=[]
322                else:
323                        self._debug("Checking result for action "+action)
324                        if self.is_action_running(action):
325                                self._join_action(action)
326                        result[action]=None
327                        if action in self.result:
328                                if 'data' in self.result[action]:
329                                        result[action]=self.result[action]['data']
330                                else:
331                                        result[action]=[]
332                self.lock.release()
333                if action in self.extraActions.keys():
334                        self._load_Store()
335                return(result)
336        #def get_result
337
338        ####
339        #Gets the status of an action
340        #Input.
341        # - action
342        #Output:
343        # - Status dict of the action
344        ####
345        def get_status(self,action=None):
346                self.lock.acquire()
347                self._debug("Checking status for action "+str(action))
348                result={}
349                if action in self.result:
350                        result=self.result[action]['status']
351                        try:
352                                errorFile=open('/usr/share/lliurex-store/files/error.json').read()
353                                errorCodes=json.loads(errorFile)
354                                errCode=str(result['status'])
355                                if errCode in errorCodes:
356                                        result['msg']=errorCodes[errCode]
357                                else:
358                                        result['msg']=u"Unknown error"
359                        except:
360                                        result['msg']=u"Unknown error"
361                self.lock.release()
362                return(result)
363        #def get_status
364
365        def load_bundles(self):
366                self.loadBundles=True
367                self._load_Store()
368
369        ####
370        #Loads the store
371        ####
372        def _load_Store(self):
373                action='load'
374                loadFunction=self._execute_class_method(action)
375                self.store=loadFunction.execute_action(action,self.store,self.loadBundles)
376        #def _load_Store
377
378        ####
379        #Loads the info related to one app
380        #Input:
381        #  - List of App objects
382        #Output:
383        #  - Dict with the related info
384        ####
385        def _get_App_Info(self,applist,launchedby=None):
386                action='get_info'
387                infoFunction=self._execute_class_method(action,None,launchedby)
388                applistInfo=infoFunction.execute_action(self.store,action,applist)
389                return(applistInfo)
390        #def _get_App_Info
391
392        ####
393        #Loads the extended info related to one app (slower)
394        #Input:
395        #  - Dict off Apps (as returned by _get_app_info)
396        #Output:
397        #  - Dict with the related info
398        ####
399        def _get_Extended_App_Info(self,applistInfo,launchedby=None,fullsearch=True):
400                #Check if there's any plugin for the distinct type of packages
401                action='pkginfo'
402                typeDict={}
403                result={}
404                result['data']=[]
405                result['status']={'status':0,'msg':''}
406                for appInfo in applistInfo:
407#                       result['data'].append(appInfo)
408                        package_type=self._check_package_type(appInfo)
409                        if package_type in typeDict:
410                                typeDict[package_type].append(appInfo)
411                        else:
412                                typeDict[package_type]=[appInfo]
413                for package_type in typeDict:
414                        self._debug("Checking plugin for "+action+ " "+package_type)
415                        if package_type in self.registeredPlugins[action]:
416#                               result['data']=[]
417                                #Only search full info if it's required
418                                if (fullsearch==False and package_type=='deb'):
419                                        result['data'].extend(typeDict[package_type])
420                                        continue
421                                self._debug("Retrieving info for "+str(typeDict[package_type]))
422                                pkgInfoFunction=self._execute_class_method(action,package_type,launchedby)
423#                               result.update(pkgInfoFunction.execute_action(action,typeDict[package_type]))
424                                result['data'].extend(pkgInfoFunction.execute_action(action,typeDict[package_type])['data'])
425#                               result['status']=pkgInfoFunction.execute_action(action,typeDict[package_type])['status']
426#                               result=pkgInfoFunction.execute_action(action,typeDict[package_type])
427                        else:
428                                result['data'].append(appInfo)
429                return(result)
430        #def _get_Extended_App_Info
431
432        def _list_sections(self,searchItem='',action='list_sections',launchedby=None):
433                result={}
434                self._debug("Retrieving all sections")
435                data={}
436                status={}
437                if action in self.registeredPlugins.keys():
438                        self._debug("Plugin for generic search: "+self.registeredPlugins[action]['*'])
439                        finder=self.registeredPlugins[action][('*')]
440                        searchFunction=eval(finder+"()")
441                        result=searchFunction.execute_action(self.store,action,searchItem)
442                        status=result['status']
443                        data=result['data']
444                else:
445                        print("No plugin for action "+action)
446                self.result[action]['data']=data
447                self.result[action]['status']=status
448                self._debug("Sections: "+str(self.result[action]['data']))
449                self._debug("Status: "+str(self.result[action]['status']))
450
451        ####
452        #Search the store
453        #Input:
454        #  - string search
455        #Output:
456        #  - List of dicts with all the info
457        ####
458        def _search_Store(self,searchItem='',action='search',fullsearch=False,launchedby=None):
459                applist={}
460                result={}
461                aux_applist=[]
462                if action=='list':
463                        try:
464                                searchItem=' '.join(searchItem)
465                        except:
466                                searchItem=''
467                elif action=='list_sections':
468                        searchItem=''
469                elif action=='info':
470                        fullsearch=True
471                if not launchedby:
472                        launchedby=action
473                #Set the exact match to false for search method
474                exact_match=True
475                if (launchedby=='search'):
476                                exact_match=False
477                searchFunction=self._execute_class_method(action,'*',launchedby)
478                result=searchFunction.execute_action(self.store,action,searchItem,exact_match)
479                aux_applist=result['data']
480                status=result['status']
481                realAction=action
482                if status['status']==0:
483                        #1.- Get appstream metadata (faster)
484                        partialAction='get_info'
485                        self.result[partialAction]={}
486                        result=self._get_App_Info(aux_applist,launchedby)
487                        self._debug("Add result for "+partialAction)
488                        self.result[partialAction]=result
489                        #2.- Get rest of metadata (slower)
490                        partialAction='pkginfo'
491                        result=self._get_Extended_App_Info(result['data'],launchedby,fullsearch)
492                        if launchedby:
493                                realAction=launchedby
494                                self._debug("Assigned results of "+action+" to "+realAction)
495                        if (result['status']['status']==0) or (result['status']['status']==9):
496                                return_msg=True
497                                if fullsearch:
498                                        result['status']['status']=0
499                        else:
500                                return_msg=False
501                else:
502                        return_msg=False
503                self.result[launchedby]['data']=result['data']
504                self.result[launchedby]['status']=result['status']
505                return(return_msg)
506        #def _search_Store
507
508        ####
509        #Install or remove an app
510        #Input:
511        #  - String with the app name
512        #Output:
513        #  - Result of the operation
514        ####
515        def _install_remove_App(self,appName,action='install',launchedby=None):
516                self._log("Attempting to "+action +" "+appName)
517                result={}
518                return_msg=False
519                if (self._search_Store(appName,'search',True,action)):
520                        applistInfo=self.result[action]['data']
521                        typeDict={}
522                        #Check if package is installed if we want to remove it or vice versa
523                        for appInfo in applistInfo:
524#Appstream doesn't get the right status in all cases so we rely on the mechanisms given by the different plugins.
525                                if (action=='install' and appInfo['state']=='installed') or (action=='remove' and appInfo['state']=='available'):
526                                        if (action=='remove' and appInfo['state']=='available'):
527                                                        self.result[action]['status']={appInfo['package']:3}
528                                                        self.result[action]['status']={'status':3}
529                                        else:
530                                                self.result[action]['status']={appInfo['package']:4}
531                                                self.result[action]['status']={'status':4}
532                                                pass
533                                        return_msg=False
534                                        typeDict={}
535                                        break
536                               
537                                package_type=self._check_package_type(appInfo)
538                                if package_type in typeDict:
539                                        typeDict[package_type].append(appInfo)
540                                else:
541                                        typeDict[package_type]=[appInfo]
542
543                        for package_type in typeDict:
544                                self._debug("Checking plugin for "+action+ " "+package_type)
545                                if package_type in self.registeredPlugins[action]:
546                                        installFunction=self._execute_class_method(action,package_type,action)
547                                        if package_type=='zmd':
548                                        #If it's a zmd the zomando must be present in the system
549                                                zmdAppInfo=[]
550                                                for zmdPackage in typeDict[package_type]:
551                                                        zmdInfo={}
552                                                        self._debug("Cheking presence of zmd "+ zmdPackage['package'])
553                                                        zmd='/usr/share/zero-center/zmds/'+appInfo['package']+'.zmd'
554                                                        if not os.path.exists(zmd):
555                                                                zmdInfo['package']=zmdPackage['package']
556                                                                zmdAppInfo.append(zmdInfo)
557                                                if zmdAppInfo:
558                                                        self._debug("Installing needed packages")
559                                                        installAuxFunction=self._execute_class_method(action,"deb",action)
560                                                        result=installAuxFunction.execute_action(action,zmdAppInfo)
561                                                       
562                                        result=installFunction.execute_action(action,typeDict[package_type])
563                                        self.result[action]=result
564                                        if result['status']['status']==0:
565                                                #Mark the apps as installed or available
566                                                for app in typeDict[package_type]:
567                                                        if action=='install':
568                                                                app['appstream_id'].set_state(1)
569                                                                self._debug("App state changed to installed")
570                                                        else:
571                                                                app['appstream_id'].set_state(2)
572                                                                self._debug("App state changed to available")
573                                        return_msg=True
574                self._log("Result "+action +": "+str(self.result[action]))
575                return(return_msg)
576        #def install_App
577       
578        ####
579        #Check the package type
580        #Input:
581        # - AppInfo dict (element of the list returned by _get_app_info)
582        #Output:
583        # - String with the type (deb, sh, zmd...)
584        ####
585        def _check_package_type(self,appInfo):
586                #Standalone installers must have the subcategory "installer"
587                #Zomandos must have the subcategory "Zomando"
588                self._debug("Checking package type for app "+appInfo['name'])
589                if "Zomando" in appInfo['categories']:
590                        return_msg="zmd"
591                else:
592                #Standalone installers must have an installerUrl field loaded from a bundle type=script description
593                        if appInfo['installerUrl']!='':
594                                return_msg="sh"
595                        elif appInfo['appImage']!='':
596                                return_msg="appimage"
597                        else:
598                                return_msg="deb"
599                return(return_msg)
600        #def _check_package_type
Note: See TracBrowser for help on using the repository browser.