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

Last change on this file since 6547 was 6547, checked in by Juanma, 2 years ago

Removed text messages from loadstore class

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