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

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

Added snap support. Minor changes

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