Mercurial > libervia-backend
view frontends/src/jp/base.py @ 1642:7ec7ce9cdc4c
jp (base): progressStarted signals are cached until progress_id is known, this avoid missing the progression if we have the signal before the id.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 22 Nov 2015 17:37:19 +0100 |
parents | 44a14f83e64b |
children | d17772b0fe22 |
line wrap: on
line source
#! /usr/bin/python # -*- coding: utf-8 -*- # jp: a SAT command line tool # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.i18n import _ ### logging ### import logging as log log.basicConfig(level=log.DEBUG, format='%(message)s') ### import sys import locale import os.path import argparse from gi.repository import GLib from glob import iglob from importlib import import_module from sat_frontends.tools.jid import JID from sat_frontends.bridge.DBus import DBusBridgeFrontend from sat.core import exceptions import sat_frontends.jp from sat_frontends.jp.constants import Const as C try: import progressbar except ImportError: log.info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar')) log.info (_('Progress bar deactivated\n--\n')) progressbar=None #consts PROG_NAME = u"jp" DESCRIPTION = """This software is a command line tool for XMPP. Get the latest version at """ + C.APP_URL COPYLEFT = u"""Copyright (C) 2009-2015 Jérôme Poisson, Adrien Cossa This program comes with ABSOLUTELY NO WARRANTY; This is free software, and you are welcome to redistribute it under certain conditions. """ PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms def unicode_decoder(arg): # Needed to have unicode strings from arguments return arg.decode(locale.getpreferredencoding()) class Jp(object): """ This class can be use to establish a connection with the bridge. Moreover, it should manage a main loop. To use it, you mainly have to redefine the method run to perform specify what kind of operation you want to perform. """ def __init__(self): """ @attribute need_loop(bool): to set by commands when loop is needed @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting, or if you want the user to stop by himself @attribute progress_success(callable): method to call when progress just started by default display a message @attribute progress_success(callable): method to call when progress is successfully finished by default display a message @attribute progress_failure(callable): method to call when progress failed by default display a message """ try: self.bridge = DBusBridgeFrontend() except exceptions.BridgeExceptionNoService: print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) sys.exit(1) except exceptions.BridgeInitError: print(_(u"Can't init bridge")) sys.exit(1) self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) self._make_parents() self.add_parser_options() self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name') self._auto_loop = False # when loop is used for internal reasons self.need_loop = False # to set by commands when loop is needed # progress attributes self._progress_id = None # TODO: manage several progress ids self.quit_on_progress_end = True @property def version(self): return self.bridge.getVersion() @property def progress_id(self): return self._progress_id @progress_id.setter def progress_id(self, value): self._progress_id = value self.replayCache('progress_ids_cache') @property def watch_progress(self): try: self.pbar except AttributeError: return False else: return True @watch_progress.setter def watch_progress(self, watch_progress): if watch_progress: self.pbar = None @property def verbosity(self): try: return self.args.verbose except AttributeError: return 0 def replayCache(self, cache_attribute): """Replay cached signals @param cache_attribute(str): name of the attribute containing the cache if the attribute doesn't exist, there is no cache and the call is ignored else the cache must be a list of tuples containing the replay callback as first item, then the arguments to use """ try: cache = getattr(self, cache_attribute) except AttributeError: pass else: for cache_data in cache: cache_data[0](*cache_data[1:]) def disp(self, msg, verbosity=0, error=False): """Print a message to user @param msg(unicode): message to print @param verbosity(int): minimal verbosity to display the message @param error(bool): if True, print to stderr instead of stdout """ if self.verbosity >= verbosity: if error: print >>sys.stderr,msg else: print msg def addOnQuitCallback(self, callback, *args, **kwargs): """Add a callback which will be called on quit command @param callback(callback): method to call """ try: callbacks_list = self._onQuitCallbacks except AttributeError: callbacks_list = self._onQuitCallbacks = [] finally: callbacks_list.append((callback, args, kwargs)) def _make_parents(self): self.parents = {} # we have a special case here as the start-session option is present only if connection is not needed, # so we create two similar parents, one with the option, the other one without it for parent_name in ('profile', 'profile_session'): parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) parent.add_argument("--pwd", action="store", type=unicode, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _(u"Connect the profile before doing anything else") profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) if progressbar: progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) def add_parser_options(self): self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) def import_commands(self): """ Automaticaly import commands to jp looks from modules names cmd_*.py in jp path and import them """ path = os.path.dirname(sat_frontends.jp.__file__) modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, "cmd_*.py")))) for module_name in modules: module = import_module("sat_frontends.jp."+module_name) try: self.import_command_module(module) except ImportError: continue def import_command_module(self, module): """ Add commands from a module to jp @param module: module containing commands """ try: for classname in module.__commands__: cls = getattr(module, classname) except AttributeError: log.warning(_("Invalid module %s") % module) raise ImportError cls(self) def run(self, args=None): self.args = self.parser.parse_args(args) self.args.func() if self.need_loop or self._auto_loop: self._start_loop() def _start_loop(self): self.loop = GLib.MainLoop() try: self.loop.run() except KeyboardInterrupt: log.info(_("User interruption: good bye")) def stop_loop(self): try: self.loop.quit() except AttributeError: pass def quitFromSignal(self, errcode=0): """Same as self.quit, but from a signal handler /!\: return must be used after calling this method ! """ assert self.need_loop # XXX: python-dbus will show a traceback if we exit in a signal handler with an error code # so we use this little timeout trick to avoid it GLib.timeout_add(0, self.quit, errcode) def quit(self, errcode=0): # first the onQuitCallbacks try: callbacks_list = self._onQuitCallbacks except AttributeError: pass else: for callback, args, kwargs in callbacks_list: callback(*args, **kwargs) self.stop_loop() if errcode: sys.exit(errcode) def check_jids(self, jids): """Check jids validity, transform roster name to corresponding jids @param profile: profile name @param jids: list of jids @return: List of jids """ names2jid = {} nodes2jid = {} for contact in self.bridge.getContacts(self.profile): jid_s, attr, groups = contact _jid = JID(jid_s) try: names2jid[attr["name"].lower()] = jid_s except KeyError: pass if _jid.node: nodes2jid[_jid.node.lower()] = jid_s def expand_jid(jid): _jid = jid.lower() if _jid in names2jid: expanded = names2jid[_jid] elif _jid in nodes2jid: expanded = nodes2jid[_jid] else: expanded = jid return expanded.decode('utf-8') def check(jid): if not jid.is_valid: log.error (_("%s is not a valid JID !"), jid) self.quit(1) dest_jids=[] try: for i in range(len(jids)): dest_jids.append(expand_jid(jids[i])) check(dest_jids[i]) except AttributeError: pass return dest_jids def connect_profile(self, callback): """ Check if the profile is connected and do it if requested @param callback: method to call when profile is connected @exit: - 1 when profile is not connected and --connect is not set - 1 when the profile doesn't exists - 1 when there is a connection error """ # FIXME: need better exit codes def cant_connect(failure): log.error(_(u"Can't connect profile: {reason}").format(reason=failure)) self.quit(1) def cant_start_session(failure): log.error(_(u"Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure)) self.quit(1) self.profile = self.bridge.getProfileName(self.args.profile) if not self.profile: log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile)) self.quit(1) try: start_session = self.args.start_session except AttributeError: pass else: if start_session: self.bridge.profileStartSession(self.args.pwd, self.profile, lambda dummy: callback(), cant_start_session) self._auto_loop = True return elif not self.bridge.profileIsSessionStarted(self.profile): if not self.args.connect: log.error(_(u"Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile)) self.quit(1) else: callback() return if not hasattr(self.args, 'connect'): # a profile can be present without connect option (e.g. on profile creation/deletion) return elif self.args.connect is True: # if connection is asked, we connect the profile self.bridge.asyncConnect(self.profile, self.args.pwd, lambda dummy: callback(), cant_connect) self._auto_loop = True return else: if not self.bridge.isConnected(self.profile): log.error(_(u"Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) self.quit(1) callback() def get_full_jid(self, param_jid): """Return the full jid if possible (add main resource when find a bare jid)""" _jid = JID(param_jid) if not _jid.resource: #if the resource is not given, we try to add the main resource main_resource = self.bridge.getMainResource(param_jid, self.profile) if main_resource: return "%s/%s" % (_jid.bare, main_resource) return param_jid def progressUpdate(self): """This method is continualy called to update the progress bar""" data = self.bridge.progressGet(self.progress_id, self.profile) if data: try: size = data['size'] except KeyError: self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True) return False if self.pbar is None: #first answer, we must construct the bar self.pbar = progressbar.ProgressBar(int(size), [_(u"Progress: "),progressbar.Percentage(), " ", progressbar.Bar(), " ", progressbar.FileTransferSpeed(), " ", progressbar.ETA()]) self.pbar.start() self.pbar.update(int(data['position'])) elif self.pbar is not None: return False return True class CommandBase(object): def __init__(self, host, name, use_profile=True, use_progress=False, use_verbose=False, need_connect=None, help=None, **kwargs): """ Initialise CommandBase @param host: Jp instance @param name(unicode): name of the new command @param use_profile(bool): if True, add profile selection/connection commands @param use_progress(bool): if True, add progress bar activation commands progress* signals will be handled @param use_verbose(bool): if True, add verbosity command @param need_connect(bool, None): True if profile connection is needed False else (profile session must still be started) None to set auto value (i.e. True if use_profile is set) Can't be set if use_profile is False @param help(unicode): help message to display @param **kwargs: args passed to ArgumentParser """ try: # If we have subcommands, host is a CommandBase and we need to use host.host self.host = host.host except AttributeError: self.host = host parents = kwargs.setdefault('parents', set()) if use_profile: #self.host.parents['profile'] is an ArgumentParser with profile connection arguments if need_connect is None: need_connect = True parents.add(self.host.parents['profile' if need_connect else 'profile_session']) else: assert need_connect is None if use_progress: parents.add(self.host.parents['progress']) if use_verbose: parents.add(self.host.parents['verbose']) self.parser = host.subparsers.add_parser(name, help=help, **kwargs) if hasattr(self, "subcommands"): self.subparsers = self.parser.add_subparsers() else: self.parser.set_defaults(func=self.run) self.add_parser_options() @property def args(self): return self.host.args @property def need_loop(self): return self.host.need_loop @need_loop.setter def need_loop(self, value): self.host.need_loop = value @property def profile(self): return self.host.profile @property def progress_id(self): return self.host.progress_id @progress_id.setter def progress_id(self, value): self.host.progress_id = value def progressStartedHandler(self, uid, metadata, profile): if profile != self.profile: return if self.progress_id is None: # the progress started message can be received before the id # so we keep progressStarted signals in cache to replay they # when the progress_id is received cache_data = (self.progressStartedHandler, uid, metadata, profile) try: self.host.progress_ids_cache.append(cache_data) except AttributeError: self.host.progress_ids_cache = [cache_data] else: if self.host.watch_progress and uid == self.progress_id: self.onProgressStarted(metadata) GLib.timeout_add(PROGRESS_DELAY, self.host.progressUpdate) def progressFinishedHandler(self, uid, metadata, profile): if profile != self.profile: return if uid == self.progress_id: try: self.host.pbar.finish() except AttributeError: pass self.onProgressFinished(metadata) if self.host.quit_on_progress_end: self.host.quit() def progressErrorHandler(self, uid, message, profile): if profile != self.profile: return if uid == self.progress_id: if self.args.progress: self.disp('') # progress is not finished, so we skip a line if self.host.quit_on_progress_end: self.onProgressError(message) self.host.quitFromSignal(1) def onProgressStarted(self, metadata): self.disp(_(u"Operation started"), 2) def onProgressFinished(self, metadata): self.disp(_(u"Operation successfully finished"), 2) def onProgressError(self, error_msg): self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True) def disp(self, msg, verbosity=0, error=False): return self.host.disp(msg, verbosity, error) def add_parser_options(self): try: subcommands = self.subcommands except AttributeError: # We don't have subcommands, the class need to implements add_parser_options raise NotImplementedError # now we add subcommands to ourself for cls in subcommands: cls(self) def run(self): try: if self.args.profile: connect_profile = self.host.connect_profile except AttributeError: # the command doesn't need to connect profile pass else: connect_profile(self.connected) try: show_progress = self.args.progress except AttributeError: # the command doesn't use progress bar pass else: if show_progress: self.host.watch_progress = True # we need to register the following signal even if we don't display the progress bar self.host.bridge.register("progressStarted", self.progressStartedHandler) self.host.bridge.register("progressFinished", self.progressFinishedHandler) self.host.bridge.register("progressError", self.progressErrorHandler) def connected(self): if not self.need_loop: self.host.stop_loop() class CommandAnswering(CommandBase): """Specialised commands which answer to specific actions to manage action_types answer, """ action_callbacks = {} # XXX: set managed action types in an dict here: # key is the action_type, value is the callable # which will manage the answer. profile filtering is # already managed when callback is called def onActionNew(self, action_data, action_id, security_limit, profile): if profile != self.profile: return try: action_type = action_data['meta_type'] except KeyError: pass else: try: callback = self.action_callbacks[action_type] except KeyError: pass else: callback(action_data, action_id, security_limit, profile) def connected(self): """Auto reply to confirmations requests""" self.need_loop = True super(CommandAnswering, self).connected() self.host.bridge.register("actionNew", self.onActionNew) actions = self.host.bridge.actionsGet(self.profile) for action_data, action_id, security_limit in actions: self.onActionNew(action_data, action_id, security_limit, self.profile)