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

Last change on this file since 7119 was 7119, checked in by Juanma, 21 months ago

WIP on bundles support

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