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

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

Minor bugfix in storeManager

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