Mercurial > libervia-backend
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) |