comparison frontends/src/jp/base.py @ 1606:de785fcf9a7b

jp (base, file): file command and progress fixes and adaptation to new API: - progress use new API, and signals to monitor progression and avoid use of progressGet before progress is actually started - progress behaviour on event is managed by callbacks which are Jp attributes - progress with unknown or 0 size don't show the progress bar - better handling of errors - CommandAnswering is update to manage actions instead of the deprecated askConfirmation - managed actions use callback easy to associate with an action type. - file command is updated to manage these changes and the recent changes in backend behaviour - verbosity is used to display more or less message when sending/receiving a file - destination path can be specified in file receive - file receive doesn't stop yet, still need some work in the backend
author Goffi <goffi@goffi.org>
date Sun, 15 Nov 2015 23:42:21 +0100
parents 0aded9648c5c
children ec48b35309dc
comparison
equal deleted inserted replaced
1605:0aded9648c5c 1606:de785fcf9a7b
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21
22 global pbar_available
23 pbar_available = True #checked before using ProgressBar
24 21
25 ### logging ### 22 ### logging ###
26 import logging as log 23 import logging as log
27 log.basicConfig(level=log.DEBUG, 24 log.basicConfig(level=log.DEBUG,
28 format='%(message)s') 25 format='%(message)s')
30 27
31 import sys 28 import sys
32 import locale 29 import locale
33 import os.path 30 import os.path
34 import argparse 31 import argparse
35 from gi.repository import GLib, GObject 32 from gi.repository import GLib
36 from glob import iglob 33 from glob import iglob
37 from importlib import import_module 34 from importlib import import_module
38 from sat_frontends.tools.jid import JID 35 from sat_frontends.tools.jid import JID
39 from sat_frontends.bridge.DBus import DBusBridgeFrontend 36 from sat_frontends.bridge.DBus import DBusBridgeFrontend
40 from sat.core import exceptions 37 from sat.core import exceptions
46 log.info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar')) 43 log.info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
47 log.info (_('Progress bar deactivated\n--\n')) 44 log.info (_('Progress bar deactivated\n--\n'))
48 progressbar=None 45 progressbar=None
49 46
50 #consts 47 #consts
51 prog_name = u"jp" 48 PROG_NAME = u"jp"
52 description = """This software is a command line tool for XMPP. 49 DESCRIPTION = """This software is a command line tool for XMPP.
53 Get the latest version at """ + C.APP_URL 50 Get the latest version at """ + C.APP_URL
54 51
55 copyleft = u"""Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (aka Goffi) 52 COPYLEFT = u"""Copyright (C) 2009-2015 Jérôme Poisson, Adrien Cossa
56 This program comes with ABSOLUTELY NO WARRANTY; 53 This program comes with ABSOLUTELY NO WARRANTY;
57 This is free software, and you are welcome to redistribute it under certain conditions. 54 This is free software, and you are welcome to redistribute it under certain conditions.
58 """ 55 """
56
57 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms
59 58
60 59
61 def unicode_decoder(arg): 60 def unicode_decoder(arg):
62 # Needed to have unicode strings from arguments 61 # Needed to have unicode strings from arguments
63 return arg.decode(locale.getpreferredencoding()) 62 return arg.decode(locale.getpreferredencoding())
71 To use it, you mainly have to redefine the method run to perform 70 To use it, you mainly have to redefine the method run to perform
72 specify what kind of operation you want to perform. 71 specify what kind of operation you want to perform.
73 72
74 """ 73 """
75 def __init__(self): 74 def __init__(self):
75 """
76
77 @attribute need_loop(bool): to set by commands when loop is needed
78 @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting,
79 or if you want the user to stop by himself
80 @attribute progress_success(callable): method to call when progress just started
81 by default display a message
82 @attribute progress_success(callable): method to call when progress is successfully finished
83 by default display a message
84 @attribute progress_failure(callable): method to call when progress failed
85 by default display a message
86 """
76 try: 87 try:
77 self.bridge = DBusBridgeFrontend() 88 self.bridge = DBusBridgeFrontend()
78 except exceptions.BridgeExceptionNoService: 89 except exceptions.BridgeExceptionNoService:
79 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) 90 print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
80 sys.exit(1) 91 sys.exit(1)
81 except exceptions.BridgeInitError: 92 except exceptions.BridgeInitError:
82 print(_(u"Can't init bridge")) 93 print(_(u"Can't init bridge"))
83 sys.exit(1) 94 sys.exit(1)
84 95
85 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, 96 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
86 description=description) 97 description=DESCRIPTION)
87 98
88 self._make_parents() 99 self._make_parents()
89 self.add_parser_options() 100 self.add_parser_options()
90 self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name') 101 self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name')
91 self._auto_loop = False # when loop is used for internal reasons 102 self._auto_loop = False # when loop is used for internal reasons
92 self.need_loop = False # to set by commands when loop is needed 103 self.need_loop = False # to set by commands when loop is needed
104
105 # progress attributes
93 self._progress_id = None # TODO: manage several progress ids 106 self._progress_id = None # TODO: manage several progress ids
94 self.quit_on_progress_end = True # set to False if you manage yourself exiting, or if you want the user to stop by himself 107 self.quit_on_progress_end = True
108 self.progress_started = lambda dummy: self.disp(_(u"Operation started"), 2)
109 self.progress_success = lambda dummy: self.disp(_(u"Operation successfully finished"), 2)
110 self.progress_failure = lambda dummy: self.disp(_(u"Error while doing operation"), error=True)
95 111
96 @property 112 @property
97 def version(self): 113 def version(self):
98 return self.bridge.getVersion() 114 return self.bridge.getVersion()
99 115
103 119
104 @progress_id.setter 120 @progress_id.setter
105 def progress_id(self, value): 121 def progress_id(self, value):
106 self._progress_id = value 122 self._progress_id = value
107 123
124 @property
125 def watch_progress(self):
126 try:
127 self.pbar
128 except AttributeError:
129 return False
130 else:
131 return True
132
133 @watch_progress.setter
134 def watch_progress(self, watch_progress):
135 if watch_progress:
136 self.pbar = None
108 137
109 @property 138 @property
110 def verbosity(self): 139 def verbosity(self):
111 try: 140 try:
112 return self.args.verbose 141 return self.args.verbose
163 192
164 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) 193 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
165 verbose_parent.add_argument('--verbose', '-v', action='count', help=_(u"Add a verbosity level (can be used multiple times)")) 194 verbose_parent.add_argument('--verbose', '-v', action='count', help=_(u"Add a verbosity level (can be used multiple times)"))
166 195
167 def add_parser_options(self): 196 def add_parser_options(self):
168 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': prog_name, 'version': self.version, 'copyleft': copyleft})) 197 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
169 198
170 def import_commands(self): 199 def import_commands(self):
171 """ Automaticaly import commands to jp 200 """ Automaticaly import commands to jp
172 looks from modules names cmd_*.py in jp path and import them 201 looks from modules names cmd_*.py in jp path and import them
173 202
342 371
343 def get_full_jid(self, param_jid): 372 def get_full_jid(self, param_jid):
344 """Return the full jid if possible (add main resource when find a bare jid)""" 373 """Return the full jid if possible (add main resource when find a bare jid)"""
345 _jid = JID(param_jid) 374 _jid = JID(param_jid)
346 if not _jid.resource: 375 if not _jid.resource:
347 #if the resource is not given, we try to add the last known resource 376 #if the resource is not given, we try to add the main resource
348 last_resource = self.bridge.getMainResource(param_jid, self.profile) 377 main_resource = self.bridge.getMainResource(param_jid, self.profile)
349 if last_resource: 378 if main_resource:
350 return "%s/%s" % (_jid.bare, last_resource) 379 return "%s/%s" % (_jid.bare, main_resource)
351 return param_jid 380 return param_jid
352 381
353 def watch_progress(self): 382 def _onProgressStarted(self, uid, profile):
354 self.pbar = None 383 if profile != self.profile:
355 GObject.timeout_add(10, self._progress_cb) 384 return
385 self.progress_started(None)
386 if self.watch_progress and self.progress_id and uid == self.progress_id:
387 GLib.timeout_add(PROGRESS_DELAY, self._progress_cb)
388
389 def _onProgressFinished(self, uid, profile):
390 if profile != self.profile:
391 return
392 if uid == self.progress_id:
393 try:
394 self.pbar.finish()
395 except AttributeError:
396 pass
397 self.progress_success(None)
398 if self.quit_on_progress_end:
399 self.quit()
400
401 def _onProgressError(self, uid, profile):
402 if profile != self.profile:
403 return
404 if uid == self.progress_id:
405 self.disp('') # progress is not finished, so we skip a line
406 if self.quit_on_progress_end:
407 self.progress_failure(None)
408 self.quitFromSignal(1)
356 409
357 def _progress_cb(self): 410 def _progress_cb(self):
358 if self.progress_id: 411 """This method is continualy called to update the progress bar"""
359 data = self.bridge.getProgress(self.progress_id, self.profile) 412 data = self.bridge.progressGet(self.progress_id, self.profile)
360 if data: 413 if data:
361 if not data['position']: 414 try:
362 data['position'] = '0' 415 size = data['size']
363 if not self.pbar: 416 except KeyError:
364 #first answer, we must construct the bar 417 self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True)
365 self.pbar = progressbar.ProgressBar(int(data['size']),
366 [_("Progress: "),progressbar.Percentage(),
367 " ",
368 progressbar.Bar(),
369 " ",
370 progressbar.FileTransferSpeed(),
371 " ",
372 progressbar.ETA()])
373 self.pbar.start()
374
375 self.pbar.update(int(data['position']))
376
377 elif self.pbar:
378 self.pbar.finish()
379 if self.quit_on_progress_end:
380 self.quit()
381 return False 418 return False
419 if self.pbar is None:
420 #first answer, we must construct the bar
421 self.pbar = progressbar.ProgressBar(int(size),
422 [_(u"Progress: "),progressbar.Percentage(),
423 " ",
424 progressbar.Bar(),
425 " ",
426 progressbar.FileTransferSpeed(),
427 " ",
428 progressbar.ETA()])
429 self.pbar.start()
430
431 self.pbar.update(int(data['position']))
432
433 elif self.pbar is not None:
434 return False
382 435
383 return True 436 return True
384 437
385 438
386 class CommandBase(object): 439 class CommandBase(object):
389 """ Initialise CommandBase 442 """ Initialise CommandBase
390 @param host: Jp instance 443 @param host: Jp instance
391 @param name(unicode): name of the new command 444 @param name(unicode): name of the new command
392 @param use_profile(bool): if True, add profile selection/connection commands 445 @param use_profile(bool): if True, add profile selection/connection commands
393 @param use_progress(bool): if True, add progress bar activation commands 446 @param use_progress(bool): if True, add progress bar activation commands
447 progress* signals will be handled
394 @param use_verbose(bool): if True, add verbosity command 448 @param use_verbose(bool): if True, add verbosity command
395 @param need_connect(bool, None): True if profile connection is needed 449 @param need_connect(bool, None): True if profile connection is needed
396 False else (profile session must still be started) 450 False else (profile session must still be started)
397 None to set auto value (i.e. True if use_profile is set) 451 None to set auto value (i.e. True if use_profile is set)
398 Can't be set if use_profile is False 452 Can't be set if use_profile is False
474 pass 528 pass
475 else: 529 else:
476 connect_profile(self.connected) 530 connect_profile(self.connected)
477 531
478 try: 532 try:
479 if self.args.progress: 533 show_progress = self.args.progress
480 watch_progress = self.host.watch_progress
481 except AttributeError: 534 except AttributeError:
482 # the command doesn't use progress bar 535 # the command doesn't use progress bar
483 pass 536 pass
484 else: 537 else:
485 watch_progress() 538 if show_progress:
539 self.host.watch_progress = True
540 # we need to register the following signal even if we don't display the progress bas
541 self.host.bridge.register("progressStarted", self.host._onProgressStarted)
542 self.host.bridge.register("progressFinished", self.host._onProgressFinished)
543 self.host.bridge.register("progressError", self.host._onProgressError)
486 544
487 def connected(self): 545 def connected(self):
488 if not self.need_loop: 546 if not self.need_loop:
489 self.host.stop_loop() 547 self.host.stop_loop()
490 548
491 549
492 class CommandAnswering(CommandBase): 550 class CommandAnswering(CommandBase):
493 #FIXME: temp, will be refactored when progress_bar/confirmations will be refactored 551 """Specialised commands which answer to specific actions
494 552
495 def _ask_confirmation(self, confirm_id, confirm_type, data, profile): 553 to manage action_types answer,
496 """ Callback used for file transfer, accept files depending on parameters""" 554 """
555 action_callbacks = {} # XXX: set managed action types in an dict here:
556 # key is the action_type, value is the callable
557 # which will manage the answer. profile filtering is
558 # already managed when callback is called
559
560 def onActionNew(self, action_data, action_id, security_limit, profile):
497 if profile != self.profile: 561 if profile != self.profile:
498 debug("Ask confirmation ignored: not our profile")
499 return 562 return
500 if confirm_type == self.confirm_type: 563 try:
501 if self.dest_jids and not JID(data['from']).bare in [JID(_jid).bare for _jid in self.dest_jids()]: 564 action_type = action_data['meta_type']
502 return #file is not sent by a filtered jid 565 except KeyError:
566 pass
567 else:
568 try:
569 callback = self.action_callbacks[action_type]
570 except KeyError:
571 pass
503 else: 572 else:
504 self.ask(data, confirm_id) 573 callback(action_data, action_id, security_limit, profile)
505
506 def ask(self):
507 """
508 The return value is used to answer to the bridge.
509 @return: bool or dict
510 """
511 raise NotImplementedError
512 574
513 def connected(self): 575 def connected(self):
514 """Auto reply to confirmations requests""" 576 """Auto reply to confirmations requests"""
515 self.need_loop = True 577 self.need_loop = True
516 super(CommandAnswering, self).connected() 578 super(CommandAnswering, self).connected()
517 # we watch confirmation signals 579 self.host.bridge.register("actionNew", self.onActionNew)
518 self.host.bridge.register("ask_confirmation", self._ask_confirmation)
519
520 #and we ask those we have missed
521 for confirm_id, confirm_type, data in self.host.bridge.getWaitingConf(self.profile):
522 self._ask_confirmation(confirm_id, confirm_type, data, self.profile)