Mercurial > libervia-backend
comparison sat_frontends/jp/base.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/jp/base.py@340128e0b354 |
children | 4011e4ee3151 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # jp: a SAT command line tool | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
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/>. | |
19 | |
20 from sat.core.i18n import _ | |
21 | |
22 ### logging ### | |
23 import logging as log | |
24 log.basicConfig(level=log.DEBUG, | |
25 format='%(message)s') | |
26 ### | |
27 | |
28 import sys | |
29 import locale | |
30 import os.path | |
31 import argparse | |
32 from glob import iglob | |
33 from importlib import import_module | |
34 from sat_frontends.tools.jid import JID | |
35 from sat.tools import config | |
36 from sat.tools.common import dynamic_import | |
37 from sat.tools.common import uri | |
38 from sat.core import exceptions | |
39 import sat_frontends.jp | |
40 from sat_frontends.jp.constants import Const as C | |
41 from sat_frontends.tools import misc | |
42 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI | |
43 import shlex | |
44 from collections import OrderedDict | |
45 | |
46 ## bridge handling | |
47 # we get bridge name from conf and initialise the right class accordingly | |
48 main_config = config.parseMainConf() | |
49 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') | |
50 | |
51 | |
52 # TODO: move loops handling in a separated module | |
53 if 'dbus' in bridge_name: | |
54 from gi.repository import GLib | |
55 | |
56 | |
57 class JPLoop(object): | |
58 | |
59 def __init__(self): | |
60 self.loop = GLib.MainLoop() | |
61 | |
62 def run(self): | |
63 self.loop.run() | |
64 | |
65 def quit(self): | |
66 self.loop.quit() | |
67 | |
68 def call_later(self, delay, callback, *args): | |
69 """call a callback repeatedly | |
70 | |
71 @param delay(int): delay between calls in ms | |
72 @param callback(callable): method to call | |
73 if the callback return True, the call will continue | |
74 else the calls will stop | |
75 @param *args: args of the callbac | |
76 """ | |
77 GLib.timeout_add(delay, callback, *args) | |
78 | |
79 else: | |
80 print u"can't start jp: only D-Bus bridge is currently handled" | |
81 sys.exit(C.EXIT_ERROR) | |
82 # FIXME: twisted loop can be used when jp can handle fully async bridges | |
83 # from twisted.internet import reactor | |
84 | |
85 # class JPLoop(object): | |
86 | |
87 # def run(self): | |
88 # reactor.run() | |
89 | |
90 # def quit(self): | |
91 # reactor.stop() | |
92 | |
93 # def _timeout_cb(self, args, callback, delay): | |
94 # ret = callback(*args) | |
95 # if ret: | |
96 # reactor.callLater(delay, self._timeout_cb, args, callback, delay) | |
97 | |
98 # def call_later(self, delay, callback, *args): | |
99 # delay = float(delay) / 1000 | |
100 # reactor.callLater(delay, self._timeout_cb, args, callback, delay) | |
101 | |
102 if bridge_name == "embedded": | |
103 from sat.core import sat_main | |
104 sat = sat_main.SAT() | |
105 | |
106 if sys.version_info < (2, 7, 3): | |
107 # XXX: shlex.split only handle unicode since python 2.7.3 | |
108 # this is a workaround for older versions | |
109 old_split = shlex.split | |
110 new_split = (lambda s, *a, **kw: [t.decode('utf-8') for t in old_split(s.encode('utf-8'), *a, **kw)] | |
111 if isinstance(s, unicode) else old_split(s, *a, **kw)) | |
112 shlex.split = new_split | |
113 | |
114 try: | |
115 import progressbar | |
116 except ImportError: | |
117 msg = (_(u'ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') + | |
118 _(u'Progress bar deactivated\n--\n')) | |
119 print >>sys.stderr,msg.encode('utf-8') | |
120 progressbar=None | |
121 | |
122 #consts | |
123 PROG_NAME = u"jp" | |
124 DESCRIPTION = """This software is a command line tool for XMPP. | |
125 Get the latest version at """ + C.APP_URL | |
126 | |
127 COPYLEFT = u"""Copyright (C) 2009-2018 Jérôme Poisson, Adrien Cossa | |
128 This program comes with ABSOLUTELY NO WARRANTY; | |
129 This is free software, and you are welcome to redistribute it under certain conditions. | |
130 """ | |
131 | |
132 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms | |
133 | |
134 | |
135 def unicode_decoder(arg): | |
136 # Needed to have unicode strings from arguments | |
137 return arg.decode(locale.getpreferredencoding()) | |
138 | |
139 | |
140 class Jp(object): | |
141 """ | |
142 This class can be use to establish a connection with the | |
143 bridge. Moreover, it should manage a main loop. | |
144 | |
145 To use it, you mainly have to redefine the method run to perform | |
146 specify what kind of operation you want to perform. | |
147 | |
148 """ | |
149 def __init__(self): | |
150 """ | |
151 | |
152 @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting, | |
153 or if you want the user to stop by himself | |
154 @attribute progress_success(callable): method to call when progress just started | |
155 by default display a message | |
156 @attribute progress_success(callable): method to call when progress is successfully finished | |
157 by default display a message | |
158 @attribute progress_failure(callable): method to call when progress failed | |
159 by default display a message | |
160 """ | |
161 # FIXME: need_loop should be removed, everything must be async in bridge so | |
162 # loop will always be needed | |
163 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') | |
164 if bridge_module is None: | |
165 log.error(u"Can't import {} bridge".format(bridge_name)) | |
166 sys.exit(1) | |
167 | |
168 self.bridge = bridge_module.Bridge() | |
169 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) | |
170 | |
171 def _bridgeCb(self): | |
172 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, | |
173 description=DESCRIPTION) | |
174 self._make_parents() | |
175 self.add_parser_options() | |
176 self.subparsers = self.parser.add_subparsers(title=_(u'Available commands'), dest='subparser_name') | |
177 self._auto_loop = False # when loop is used for internal reasons | |
178 self._need_loop = False | |
179 | |
180 # progress attributes | |
181 self._progress_id = None # TODO: manage several progress ids | |
182 self.quit_on_progress_end = True | |
183 | |
184 # outputs | |
185 self._outputs = {} | |
186 for type_ in C.OUTPUT_TYPES: | |
187 self._outputs[type_] = OrderedDict() | |
188 self.default_output = {} | |
189 | |
190 def _bridgeEb(self, failure): | |
191 if isinstance(failure, exceptions.BridgeExceptionNoService): | |
192 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) | |
193 elif isinstance(failure, exceptions.BridgeInitError): | |
194 print(_(u"Can't init bridge")) | |
195 else: | |
196 print(_(u"Error while initialising bridge: {}".format(failure))) | |
197 sys.exit(C.EXIT_BRIDGE_ERROR) | |
198 | |
199 @property | |
200 def version(self): | |
201 return self.bridge.getVersion() | |
202 | |
203 @property | |
204 def progress_id(self): | |
205 return self._progress_id | |
206 | |
207 @progress_id.setter | |
208 def progress_id(self, value): | |
209 self._progress_id = value | |
210 self.replayCache('progress_ids_cache') | |
211 | |
212 @property | |
213 def watch_progress(self): | |
214 try: | |
215 self.pbar | |
216 except AttributeError: | |
217 return False | |
218 else: | |
219 return True | |
220 | |
221 @watch_progress.setter | |
222 def watch_progress(self, watch_progress): | |
223 if watch_progress: | |
224 self.pbar = None | |
225 | |
226 @property | |
227 def verbosity(self): | |
228 try: | |
229 return self.args.verbose | |
230 except AttributeError: | |
231 return 0 | |
232 | |
233 def replayCache(self, cache_attribute): | |
234 """Replay cached signals | |
235 | |
236 @param cache_attribute(str): name of the attribute containing the cache | |
237 if the attribute doesn't exist, there is no cache and the call is ignored | |
238 else the cache must be a list of tuples containing the replay callback as first item, | |
239 then the arguments to use | |
240 """ | |
241 try: | |
242 cache = getattr(self, cache_attribute) | |
243 except AttributeError: | |
244 pass | |
245 else: | |
246 for cache_data in cache: | |
247 cache_data[0](*cache_data[1:]) | |
248 | |
249 def disp(self, msg, verbosity=0, error=False, no_lf=False): | |
250 """Print a message to user | |
251 | |
252 @param msg(unicode): message to print | |
253 @param verbosity(int): minimal verbosity to display the message | |
254 @param error(bool): if True, print to stderr instead of stdout | |
255 @param no_lf(bool): if True, do not emit line feed at the end of line | |
256 """ | |
257 if self.verbosity >= verbosity: | |
258 if error: | |
259 if no_lf: | |
260 print >>sys.stderr,msg.encode('utf-8'), | |
261 else: | |
262 print >>sys.stderr,msg.encode('utf-8') | |
263 else: | |
264 if no_lf: | |
265 print msg.encode('utf-8'), | |
266 else: | |
267 print msg.encode('utf-8') | |
268 | |
269 def output(self, type_, name, extra_outputs, data): | |
270 if name in extra_outputs: | |
271 extra_outputs[name](data) | |
272 else: | |
273 self._outputs[type_][name]['callback'](data) | |
274 | |
275 def addOnQuitCallback(self, callback, *args, **kwargs): | |
276 """Add a callback which will be called on quit command | |
277 | |
278 @param callback(callback): method to call | |
279 """ | |
280 try: | |
281 callbacks_list = self._onQuitCallbacks | |
282 except AttributeError: | |
283 callbacks_list = self._onQuitCallbacks = [] | |
284 finally: | |
285 callbacks_list.append((callback, args, kwargs)) | |
286 | |
287 def getOutputChoices(self, output_type): | |
288 """Return valid output filters for output_type | |
289 | |
290 @param output_type: True for default, | |
291 else can be any registered type | |
292 """ | |
293 return self._outputs[output_type].keys() | |
294 | |
295 def _make_parents(self): | |
296 self.parents = {} | |
297 | |
298 # we have a special case here as the start-session option is present only if connection is not needed, | |
299 # so we create two similar parents, one with the option, the other one without it | |
300 for parent_name in ('profile', 'profile_session'): | |
301 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) | |
302 parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) | |
303 parent.add_argument("--pwd", action="store", type=unicode_decoder, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) | |
304 | |
305 profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] | |
306 | |
307 connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _(u"Connect the profile before doing anything else") | |
308 profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) | |
309 | |
310 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() | |
311 profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) | |
312 profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) | |
313 | |
314 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) | |
315 if progressbar: | |
316 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) | |
317 | |
318 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) | |
319 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) | |
320 | |
321 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) | |
322 draft_group = draft_parent.add_argument_group(_('draft handling')) | |
323 draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft")) | |
324 draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve")) | |
325 | |
326 | |
327 def make_pubsub_group(self, flags, defaults): | |
328 """generate pubsub options according to flags | |
329 | |
330 @param flags(iterable[unicode]): see [CommandBase.__init__] | |
331 @param defaults(dict[unicode, unicode]): help text for default value | |
332 key can be "service" or "node" | |
333 value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT | |
334 @return (ArgumentParser): parser to add | |
335 """ | |
336 flags = misc.FlagsHandler(flags) | |
337 parent = argparse.ArgumentParser(add_help=False) | |
338 pubsub_group = parent.add_argument_group('pubsub') | |
339 pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder, | |
340 help=_(u"Pubsub URL (xmpp or http)")) | |
341 | |
342 service_help = _(u"JID of the PubSub service") | |
343 if not flags.service: | |
344 default = defaults.pop(u'service', _(u'PEP service')) | |
345 if default is not None: | |
346 service_help += _(u" (DEFAULT: {default})".format(default=default)) | |
347 pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'', | |
348 help=service_help) | |
349 | |
350 node_help = _(u"node to request") | |
351 if not flags.node: | |
352 default = defaults.pop(u'node', _(u'standard node')) | |
353 if default is not None: | |
354 node_help += _(u" (DEFAULT: {default})".format(default=default)) | |
355 pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help) | |
356 | |
357 if flags.single_item: | |
358 item_help = (u"item to retrieve") | |
359 if not flags.item: | |
360 default = defaults.pop(u'item', _(u'last item')) | |
361 if default is not None: | |
362 item_help += _(u" (DEFAULT: {default})".format(default=default)) | |
363 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, help=item_help) | |
364 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item')) | |
365 elif flags.multi_items: | |
366 # mutiple items | |
367 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)")) | |
368 if not flags.no_max: | |
369 pubsub_group.add_argument("-m", "--max", type=int, default=10, | |
370 help=_(u"maximum number of items to get ({no_limit} to get all items)".format(no_limit=C.NO_LIMIT))) | |
371 | |
372 if flags: | |
373 raise exceptions.InternalError('unknowns flags: {flags}'.format(flags=u', '.join(flags))) | |
374 if defaults: | |
375 raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults)) | |
376 | |
377 return parent | |
378 | |
379 def add_parser_options(self): | |
380 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) | |
381 | |
382 def register_output(self, type_, name, callback, description="", default=False): | |
383 if type_ not in C.OUTPUT_TYPES: | |
384 log.error(u"Invalid output type {}".format(type_)) | |
385 return | |
386 self._outputs[type_][name] = {'callback': callback, | |
387 'description': description | |
388 } | |
389 if default: | |
390 if type_ in self.default_output: | |
391 self.disp(_(u'there is already a default output for {}, ignoring new one').format(type_)) | |
392 else: | |
393 self.default_output[type_] = name | |
394 | |
395 | |
396 def parse_output_options(self): | |
397 options = self.command.args.output_opts | |
398 options_dict = {} | |
399 for option in options: | |
400 try: | |
401 key, value = option.split(u'=', 1) | |
402 except ValueError: | |
403 key, value = option, None | |
404 options_dict[key.strip()] = value.strip() if value is not None else None | |
405 return options_dict | |
406 | |
407 def check_output_options(self, accepted_set, options): | |
408 if not accepted_set.issuperset(options): | |
409 self.disp(u"The following output options are invalid: {invalid_options}".format( | |
410 invalid_options = u', '.join(set(options).difference(accepted_set))), | |
411 error=True) | |
412 self.quit(C.EXIT_BAD_ARG) | |
413 | |
414 def import_plugins(self): | |
415 """Automaticaly import commands and outputs in jp | |
416 | |
417 looks from modules names cmd_*.py in jp path and import them | |
418 """ | |
419 path = os.path.dirname(sat_frontends.jp.__file__) | |
420 # XXX: outputs must be imported before commands as they are used for arguments | |
421 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')): | |
422 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) | |
423 for module_name in modules: | |
424 module_path = "sat_frontends.jp." + module_name | |
425 try: | |
426 module = import_module(module_path) | |
427 self.import_plugin_module(module, type_) | |
428 except ImportError as e: | |
429 self.disp(_(u"Can't import {module_path} plugin, ignoring it: {msg}".format( | |
430 module_path = module_path, | |
431 msg = e)), error=True) | |
432 except exceptions.CancelError: | |
433 continue | |
434 except exceptions.MissingModule as e: | |
435 self.disp(_(u"Missing module for plugin {name}: {missing}".format( | |
436 name = module_path, | |
437 missing = e)), error=True) | |
438 | |
439 | |
440 def import_plugin_module(self, module, type_): | |
441 """add commands or outpus from a module to jp | |
442 | |
443 @param module: module containing commands or outputs | |
444 @param type_(str): one of C_PLUGIN_* | |
445 """ | |
446 try: | |
447 class_names = getattr(module, '__{}__'.format(type_)) | |
448 except AttributeError: | |
449 log.disp(_(u"Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True) | |
450 raise ImportError | |
451 else: | |
452 for class_name in class_names: | |
453 cls = getattr(module, class_name) | |
454 cls(self) | |
455 | |
456 def get_xmpp_uri_from_http(self, http_url): | |
457 """parse HTML page at http(s) URL, and looks for xmpp: uri""" | |
458 if http_url.startswith('https'): | |
459 scheme = u'https' | |
460 elif http_url.startswith('http'): | |
461 scheme = u'http' | |
462 else: | |
463 raise exceptions.InternalError(u'An HTTP scheme is expected in this method') | |
464 self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1) | |
465 # HTTP URL, we try to find xmpp: links | |
466 try: | |
467 from lxml import etree | |
468 except ImportError: | |
469 self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) | |
470 self.host.quit(1) | |
471 import urllib2 | |
472 parser = etree.HTMLParser() | |
473 try: | |
474 root = etree.parse(urllib2.urlopen(http_url), parser) | |
475 except etree.XMLSyntaxError as e: | |
476 self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e)) | |
477 links = [] | |
478 else: | |
479 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") | |
480 if not links: | |
481 self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) | |
482 self.host.quit(1) | |
483 xmpp_uri = links[0].get('href') | |
484 return xmpp_uri | |
485 | |
486 def parse_pubsub_args(self): | |
487 if self.args.pubsub_url is not None: | |
488 url = self.args.pubsub_url | |
489 | |
490 if url.startswith('http'): | |
491 # http(s) URL, we try to retrieve xmpp one from there | |
492 url = self.get_xmpp_uri_from_http(url) | |
493 | |
494 try: | |
495 uri_data = uri.parseXMPPUri(url) | |
496 except ValueError: | |
497 self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url)) | |
498 else: | |
499 if uri_data[u'type'] == 'pubsub': | |
500 # URL is alright, we only set data not already set by other options | |
501 if not self.args.service: | |
502 self.args.service = uri_data[u'path'] | |
503 if not self.args.node: | |
504 self.args.node = uri_data[u'node'] | |
505 uri_item = uri_data.get(u'item') | |
506 if uri_item: | |
507 # there is an item in URI | |
508 # we use it only if item is not already set | |
509 # and item_last is not used either | |
510 try: | |
511 item = self.args.item | |
512 except AttributeError: | |
513 if not self.args.items: | |
514 self.args.items = [uri_item] | |
515 else: | |
516 if not item: | |
517 try: | |
518 item_last = self.args.item_last | |
519 except AttributeError: | |
520 item_last = False | |
521 if not item_last: | |
522 self.args.item = uri_item | |
523 else: | |
524 self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url)) | |
525 flags = self.args._cmd._pubsub_flags | |
526 # we check required arguments here instead of using add_arguments' required option | |
527 # because the required argument can be set in URL | |
528 if C.SERVICE in flags and not self.args.service: | |
529 self.parser.error(_(u"argument -s/--service is required")) | |
530 if C.NODE in flags and not self.args.node: | |
531 self.parser.error(_(u"argument -n/--node is required")) | |
532 if C.ITEM in flags and not self.args.item: | |
533 self.parser.error(_(u"argument -i/--item is required")) | |
534 | |
535 # FIXME: mutually groups can't be nested in a group and don't support title | |
536 # so we check conflict here. This may be fixed in Python 3, to be checked | |
537 try: | |
538 if self.args.item and self.args.item_last: | |
539 self.parser.error(_(u"--item and --item-last can't be used at the same time")) | |
540 except AttributeError: | |
541 pass | |
542 | |
543 def run(self, args=None, namespace=None): | |
544 self.args = self.parser.parse_args(args, namespace=None) | |
545 if self.args._cmd._use_pubsub: | |
546 self.parse_pubsub_args() | |
547 try: | |
548 self.args._cmd.run() | |
549 if self._need_loop or self._auto_loop: | |
550 self._start_loop() | |
551 except KeyboardInterrupt: | |
552 log.info(_("User interruption: good bye")) | |
553 | |
554 def _start_loop(self): | |
555 self.loop = JPLoop() | |
556 self.loop.run() | |
557 | |
558 def stop_loop(self): | |
559 try: | |
560 self.loop.quit() | |
561 except AttributeError: | |
562 pass | |
563 | |
564 def confirmOrQuit(self, message, cancel_message=_(u"action cancelled by user")): | |
565 """Request user to confirm action, and quit if he doesn't""" | |
566 | |
567 res = raw_input("{} (y/N)? ".format(message)) | |
568 if res not in ("y", "Y"): | |
569 self.disp(cancel_message) | |
570 self.quit(C.EXIT_USER_CANCELLED) | |
571 | |
572 def quitFromSignal(self, errcode=0): | |
573 """Same as self.quit, but from a signal handler | |
574 | |
575 /!\: return must be used after calling this method ! | |
576 """ | |
577 assert self._need_loop | |
578 # XXX: python-dbus will show a traceback if we exit in a signal handler | |
579 # so we use this little timeout trick to avoid it | |
580 self.loop.call_later(0, self.quit, errcode) | |
581 | |
582 def quit(self, errcode=0): | |
583 # first the onQuitCallbacks | |
584 try: | |
585 callbacks_list = self._onQuitCallbacks | |
586 except AttributeError: | |
587 pass | |
588 else: | |
589 for callback, args, kwargs in callbacks_list: | |
590 callback(*args, **kwargs) | |
591 | |
592 self.stop_loop() | |
593 sys.exit(errcode) | |
594 | |
595 def check_jids(self, jids): | |
596 """Check jids validity, transform roster name to corresponding jids | |
597 | |
598 @param profile: profile name | |
599 @param jids: list of jids | |
600 @return: List of jids | |
601 | |
602 """ | |
603 names2jid = {} | |
604 nodes2jid = {} | |
605 | |
606 for contact in self.bridge.getContacts(self.profile): | |
607 jid_s, attr, groups = contact | |
608 _jid = JID(jid_s) | |
609 try: | |
610 names2jid[attr["name"].lower()] = jid_s | |
611 except KeyError: | |
612 pass | |
613 | |
614 if _jid.node: | |
615 nodes2jid[_jid.node.lower()] = jid_s | |
616 | |
617 def expand_jid(jid): | |
618 _jid = jid.lower() | |
619 if _jid in names2jid: | |
620 expanded = names2jid[_jid] | |
621 elif _jid in nodes2jid: | |
622 expanded = nodes2jid[_jid] | |
623 else: | |
624 expanded = jid | |
625 return expanded.decode('utf-8') | |
626 | |
627 def check(jid): | |
628 if not jid.is_valid: | |
629 log.error (_("%s is not a valid JID !"), jid) | |
630 self.quit(1) | |
631 | |
632 dest_jids=[] | |
633 try: | |
634 for i in range(len(jids)): | |
635 dest_jids.append(expand_jid(jids[i])) | |
636 check(dest_jids[i]) | |
637 except AttributeError: | |
638 pass | |
639 | |
640 return dest_jids | |
641 | |
642 def connect_profile(self, callback): | |
643 """ Check if the profile is connected and do it if requested | |
644 | |
645 @param callback: method to call when profile is connected | |
646 @exit: - 1 when profile is not connected and --connect is not set | |
647 - 1 when the profile doesn't exists | |
648 - 1 when there is a connection error | |
649 """ | |
650 # FIXME: need better exit codes | |
651 | |
652 def cant_connect(failure): | |
653 log.error(_(u"Can't connect profile: {reason}").format(reason=failure)) | |
654 self.quit(1) | |
655 | |
656 def cant_start_session(failure): | |
657 log.error(_(u"Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure)) | |
658 self.quit(1) | |
659 | |
660 self.profile = self.bridge.profileNameGet(self.args.profile) | |
661 | |
662 if not self.profile: | |
663 log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile)) | |
664 self.quit(1) | |
665 | |
666 try: | |
667 start_session = self.args.start_session | |
668 except AttributeError: | |
669 pass | |
670 else: | |
671 if start_session: | |
672 self.bridge.profileStartSession(self.args.pwd, self.profile, lambda dummy: callback(), cant_start_session) | |
673 self._auto_loop = True | |
674 return | |
675 elif not self.bridge.profileIsSessionStarted(self.profile): | |
676 if not self.args.connect: | |
677 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)) | |
678 self.quit(1) | |
679 else: | |
680 callback() | |
681 return | |
682 | |
683 | |
684 if not hasattr(self.args, 'connect'): | |
685 # a profile can be present without connect option (e.g. on profile creation/deletion) | |
686 return | |
687 elif self.args.connect is True: # if connection is asked, we connect the profile | |
688 self.bridge.connect(self.profile, self.args.pwd, {}, lambda dummy: callback(), cant_connect) | |
689 self._auto_loop = True | |
690 return | |
691 else: | |
692 if not self.bridge.isConnected(self.profile): | |
693 log.error(_(u"Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) | |
694 self.quit(1) | |
695 | |
696 callback() | |
697 | |
698 def get_full_jid(self, param_jid): | |
699 """Return the full jid if possible (add main resource when find a bare jid)""" | |
700 _jid = JID(param_jid) | |
701 if not _jid.resource: | |
702 #if the resource is not given, we try to add the main resource | |
703 main_resource = self.bridge.getMainResource(param_jid, self.profile) | |
704 if main_resource: | |
705 return "%s/%s" % (_jid.bare, main_resource) | |
706 return param_jid | |
707 | |
708 | |
709 class CommandBase(object): | |
710 | |
711 def __init__(self, host, name, use_profile=True, use_output=False, extra_outputs=None, | |
712 need_connect=None, help=None, **kwargs): | |
713 """Initialise CommandBase | |
714 | |
715 @param host: Jp instance | |
716 @param name(unicode): name of the new command | |
717 @param use_profile(bool): if True, add profile selection/connection commands | |
718 @param use_output(bool, unicode): if not False, add --output option | |
719 @param extra_outputs(dict): list of command specific outputs: | |
720 key is output name ("default" to use as main output) | |
721 value is a callable which will format the output (data will be used as only argument) | |
722 if a key already exists with normal outputs, the extra one will be used | |
723 @param need_connect(bool, None): True if profile connection is needed | |
724 False else (profile session must still be started) | |
725 None to set auto value (i.e. True if use_profile is set) | |
726 Can't be set if use_profile is False | |
727 @param help(unicode): help message to display | |
728 @param **kwargs: args passed to ArgumentParser | |
729 use_* are handled directly, they can be: | |
730 - use_progress(bool): if True, add progress bar activation option | |
731 progress* signals will be handled | |
732 - use_verbose(bool): if True, add verbosity option | |
733 - use_pubsub(bool): if True, add pubsub options | |
734 mandatory arguments are controlled by pubsub_req | |
735 - use_draft(bool): if True, add draft handling options | |
736 ** other arguments ** | |
737 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be: | |
738 C.SERVICE: service is required | |
739 C.NODE: node is required | |
740 C.SINGLE_ITEM: only one item is allowed | |
741 @attribute need_loop(bool): to set by commands when loop is needed | |
742 """ | |
743 self.need_loop = False # to be set by commands when loop is needed | |
744 try: # If we have subcommands, host is a CommandBase and we need to use host.host | |
745 self.host = host.host | |
746 except AttributeError: | |
747 self.host = host | |
748 | |
749 # --profile option | |
750 parents = kwargs.setdefault('parents', set()) | |
751 if use_profile: | |
752 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments | |
753 if need_connect is None: | |
754 need_connect = True | |
755 parents.add(self.host.parents['profile' if need_connect else 'profile_session']) | |
756 else: | |
757 assert need_connect is None | |
758 self.need_connect = need_connect | |
759 # from this point, self.need_connect is None if connection is not needed at all | |
760 # False if session starting is needed, and True if full connection is needed | |
761 | |
762 # --output option | |
763 if use_output: | |
764 if extra_outputs is None: | |
765 extra_outputs = {} | |
766 self.extra_outputs = extra_outputs | |
767 if use_output == True: | |
768 use_output = C.OUTPUT_TEXT | |
769 assert use_output in C.OUTPUT_TYPES | |
770 self._output_type = use_output | |
771 output_parent = argparse.ArgumentParser(add_help=False) | |
772 choices = set(self.host.getOutputChoices(use_output)) | |
773 choices.update(extra_outputs) | |
774 if not choices: | |
775 raise exceptions.InternalError("No choice found for {} output type".format(use_output)) | |
776 try: | |
777 default = self.host.default_output[use_output] | |
778 except KeyError: | |
779 if u'default' in choices: | |
780 default = u'default' | |
781 elif u'simple' in choices: | |
782 default = u'simple' | |
783 else: | |
784 default = list(choices)[0] | |
785 output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_(u"select output format (default: {})".format(default))) | |
786 output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option")) | |
787 parents.add(output_parent) | |
788 else: | |
789 assert extra_outputs is None | |
790 | |
791 self._use_pubsub = kwargs.pop('use_pubsub', False) | |
792 if self._use_pubsub: | |
793 flags = kwargs.pop('pubsub_flags', []) | |
794 defaults = kwargs.pop('pubsub_defaults', {}) | |
795 parents.add(self.host.make_pubsub_group(flags, defaults)) | |
796 self._pubsub_flags = flags | |
797 | |
798 # other common options | |
799 use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')} | |
800 for param, do_use in use_opts.iteritems(): | |
801 opt=param[4:] # if param is use_verbose, opt is verbose | |
802 if opt not in self.host.parents: | |
803 raise exceptions.InternalError(u"Unknown parent option {}".format(opt)) | |
804 del kwargs[param] | |
805 if do_use: | |
806 parents.add(self.host.parents[opt]) | |
807 | |
808 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) | |
809 if hasattr(self, "subcommands"): | |
810 self.subparsers = self.parser.add_subparsers() | |
811 else: | |
812 self.parser.set_defaults(_cmd=self) | |
813 self.add_parser_options() | |
814 | |
815 @property | |
816 def args(self): | |
817 return self.host.args | |
818 | |
819 @property | |
820 def profile(self): | |
821 return self.host.profile | |
822 | |
823 @property | |
824 def verbosity(self): | |
825 return self.host.verbosity | |
826 | |
827 @property | |
828 def progress_id(self): | |
829 return self.host.progress_id | |
830 | |
831 @progress_id.setter | |
832 def progress_id(self, value): | |
833 self.host.progress_id = value | |
834 | |
835 def progressStartedHandler(self, uid, metadata, profile): | |
836 if profile != self.profile: | |
837 return | |
838 if self.progress_id is None: | |
839 # the progress started message can be received before the id | |
840 # so we keep progressStarted signals in cache to replay they | |
841 # when the progress_id is received | |
842 cache_data = (self.progressStartedHandler, uid, metadata, profile) | |
843 try: | |
844 self.host.progress_ids_cache.append(cache_data) | |
845 except AttributeError: | |
846 self.host.progress_ids_cache = [cache_data] | |
847 else: | |
848 if self.host.watch_progress and uid == self.progress_id: | |
849 self.onProgressStarted(metadata) | |
850 self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate) | |
851 | |
852 def progressFinishedHandler(self, uid, metadata, profile): | |
853 if profile != self.profile: | |
854 return | |
855 if uid == self.progress_id: | |
856 try: | |
857 self.host.pbar.finish() | |
858 except AttributeError: | |
859 pass | |
860 self.onProgressFinished(metadata) | |
861 if self.host.quit_on_progress_end: | |
862 self.host.quitFromSignal() | |
863 | |
864 def progressErrorHandler(self, uid, message, profile): | |
865 if profile != self.profile: | |
866 return | |
867 if uid == self.progress_id: | |
868 if self.args.progress: | |
869 self.disp('') # progress is not finished, so we skip a line | |
870 if self.host.quit_on_progress_end: | |
871 self.onProgressError(message) | |
872 self.host.quitFromSignal(1) | |
873 | |
874 def progressUpdate(self): | |
875 """This method is continualy called to update the progress bar""" | |
876 data = self.host.bridge.progressGet(self.progress_id, self.profile) | |
877 if data: | |
878 try: | |
879 size = data['size'] | |
880 except KeyError: | |
881 self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True) | |
882 return False | |
883 if self.host.pbar is None: | |
884 #first answer, we must construct the bar | |
885 self.host.pbar = progressbar.ProgressBar(max_value=int(size), | |
886 widgets=[_(u"Progress: "),progressbar.Percentage(), | |
887 " ", | |
888 progressbar.Bar(), | |
889 " ", | |
890 progressbar.FileTransferSpeed(), | |
891 " ", | |
892 progressbar.ETA()]) | |
893 self.host.pbar.start() | |
894 | |
895 self.host.pbar.update(int(data['position'])) | |
896 | |
897 elif self.host.pbar is not None: | |
898 return False | |
899 | |
900 self.onProgressUpdate(data) | |
901 | |
902 return True | |
903 | |
904 def onProgressStarted(self, metadata): | |
905 """Called when progress has just started | |
906 | |
907 can be overidden by a command | |
908 @param metadata(dict): metadata as sent by bridge.progressStarted | |
909 """ | |
910 self.disp(_(u"Operation started"), 2) | |
911 | |
912 def onProgressUpdate(self, metadata): | |
913 """Method called on each progress updata | |
914 | |
915 can be overidden by a command to handle progress metadata | |
916 @para metadata(dict): metadata as returned by bridge.progressGet | |
917 """ | |
918 pass | |
919 | |
920 def onProgressFinished(self, metadata): | |
921 """Called when progress has just finished | |
922 | |
923 can be overidden by a command | |
924 @param metadata(dict): metadata as sent by bridge.progressFinished | |
925 """ | |
926 self.disp(_(u"Operation successfully finished"), 2) | |
927 | |
928 def onProgressError(self, error_msg): | |
929 """Called when a progress failed | |
930 | |
931 @param error_msg(unicode): error message as sent by bridge.progressError | |
932 """ | |
933 self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True) | |
934 | |
935 def disp(self, msg, verbosity=0, error=False, no_lf=False): | |
936 return self.host.disp(msg, verbosity, error, no_lf) | |
937 | |
938 def output(self, data): | |
939 try: | |
940 output_type = self._output_type | |
941 except AttributeError: | |
942 raise exceptions.InternalError(_(u'trying to use output when use_output has not been set')) | |
943 return self.host.output(output_type, self.args.output, self.extra_outputs, data) | |
944 | |
945 def exitCb(self, msg=None): | |
946 """generic callback for success | |
947 | |
948 optionally print a message, and quit | |
949 msg(None, unicode): if not None, print this message | |
950 """ | |
951 if msg is not None: | |
952 self.disp(msg) | |
953 self.host.quit(C.EXIT_OK) | |
954 | |
955 def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR): | |
956 """generic callback for errbacks | |
957 | |
958 display failure_ then quit with generic error | |
959 @param failure_: arguments returned by errback | |
960 @param msg(unicode, None): message template | |
961 use {} if you want to display failure message | |
962 @param exit_code(int): shell exit code | |
963 """ | |
964 if msg is None: | |
965 msg = _(u"error: {}") | |
966 self.disp(msg.format(failure_), error=True) | |
967 self.host.quit(exit_code) | |
968 | |
969 def add_parser_options(self): | |
970 try: | |
971 subcommands = self.subcommands | |
972 except AttributeError: | |
973 # We don't have subcommands, the class need to implements add_parser_options | |
974 raise NotImplementedError | |
975 | |
976 # now we add subcommands to ourself | |
977 for cls in subcommands: | |
978 cls(self) | |
979 | |
980 def run(self): | |
981 """this method is called when a command is actually run | |
982 | |
983 It set stuff like progression callbacks and profile connection | |
984 You should not overide this method: you should call self.start instead | |
985 """ | |
986 # we keep a reference to run command, it may be useful e.g. for outputs | |
987 self.host.command = self | |
988 # host._need_loop is set here from our current value and not before | |
989 # as the need_loop decision must be taken only by then running command | |
990 self.host._need_loop = self.need_loop | |
991 | |
992 try: | |
993 show_progress = self.args.progress | |
994 except AttributeError: | |
995 # the command doesn't use progress bar | |
996 pass | |
997 else: | |
998 if show_progress: | |
999 self.host.watch_progress = True | |
1000 # we need to register the following signal even if we don't display the progress bar | |
1001 self.host.bridge.register_signal("progressStarted", self.progressStartedHandler) | |
1002 self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler) | |
1003 self.host.bridge.register_signal("progressError", self.progressErrorHandler) | |
1004 | |
1005 if self.need_connect is not None: | |
1006 self.host.connect_profile(self.connected) | |
1007 else: | |
1008 self.start() | |
1009 | |
1010 def connected(self): | |
1011 """this method is called when profile is connected (or session is started) | |
1012 | |
1013 this method is only called when use_profile is True | |
1014 most of time you should override self.start instead of this method, but if loop | |
1015 if not always needed depending on your arguments, you may override this method, | |
1016 but don't forget to call the parent one (i.e. this one) after self.need_loop is set | |
1017 """ | |
1018 if not self.need_loop: | |
1019 self.host.stop_loop() | |
1020 self.start() | |
1021 | |
1022 def start(self): | |
1023 """This is the starting point of the command, this method should be overriden | |
1024 | |
1025 at this point, profile are connected if needed | |
1026 """ | |
1027 pass | |
1028 | |
1029 | |
1030 class CommandAnswering(CommandBase): | |
1031 """Specialised commands which answer to specific actions | |
1032 | |
1033 to manage action_types answer, | |
1034 """ | |
1035 action_callbacks = {} # XXX: set managed action types in a dict here: | |
1036 # key is the action_type, value is the callable | |
1037 # which will manage the answer. profile filtering is | |
1038 # already managed when callback is called | |
1039 | |
1040 def __init__(self, *args, **kwargs): | |
1041 super(CommandAnswering, self).__init__(*args, **kwargs) | |
1042 self.need_loop = True | |
1043 | |
1044 def onActionNew(self, action_data, action_id, security_limit, profile): | |
1045 if profile != self.profile: | |
1046 return | |
1047 try: | |
1048 action_type = action_data['meta_type'] | |
1049 except KeyError: | |
1050 try: | |
1051 xml_ui = action_data["xmlui"] | |
1052 except KeyError: | |
1053 pass | |
1054 else: | |
1055 self.onXMLUI(xml_ui) | |
1056 else: | |
1057 try: | |
1058 callback = self.action_callbacks[action_type] | |
1059 except KeyError: | |
1060 pass | |
1061 else: | |
1062 callback(action_data, action_id, security_limit, profile) | |
1063 | |
1064 def onXMLUI(self, xml_ui): | |
1065 """Display a dialog received from the backend. | |
1066 | |
1067 @param xml_ui (unicode): dialog XML representation | |
1068 """ | |
1069 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module | |
1070 # should be available in the future | |
1071 # TODO: XMLUI module | |
1072 ui = ET.fromstring(xml_ui.encode('utf-8')) | |
1073 dialog = ui.find("dialog") | |
1074 if dialog is not None: | |
1075 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") | |
1076 | |
1077 def connected(self): | |
1078 """Auto reply to confirmations requests""" | |
1079 self.need_loop = True | |
1080 super(CommandAnswering, self).connected() | |
1081 self.host.bridge.register_signal("actionNew", self.onActionNew) | |
1082 actions = self.host.bridge.actionsGet(self.profile) | |
1083 for action_data, action_id, security_limit in actions: | |
1084 self.onActionNew(action_data, action_id, security_limit, self.profile) |