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

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

added appImage plugin

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