comparison sat_frontends/jp/base.py @ 3040:fee60f17ebac

jp: jp asyncio port: /!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\ This patch implements the port of jp to asyncio, so it is now correctly using the bridge asynchronously, and it can be used with bridges like `pb`. This also simplify the code, notably for things which were previously implemented with many callbacks (like pagination with RSM). During the process, some behaviours have been modified/fixed, in jp and backends, check diff for details.
author Goffi <goffi@goffi.org>
date Wed, 25 Sep 2019 08:56:41 +0200
parents ab2696e34d29
children 3df611adb598
comparison
equal deleted inserted replaced
3039:a1bc34f90fa5 3040:fee60f17ebac
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
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 import asyncio
20 from sat.core.i18n import _ 21 from sat.core.i18n import _
21 22
22 ### logging ### 23 ### logging ###
23 import logging as log 24 import logging as log
24 log.basicConfig(level=log.DEBUG, 25 log.basicConfig(level=log.WARNING,
25 format='%(message)s') 26 format='[%(name)s] %(message)s')
26 ### 27 ###
27 28
28 import sys 29 import sys
29 import locale 30 import os
30 import os.path 31 import os.path
31 import argparse 32 import argparse
33 import inspect
34 from pathlib import Path
32 from glob import iglob 35 from glob import iglob
33 from importlib import import_module 36 from importlib import import_module
34 from sat_frontends.tools.jid import JID 37 from sat_frontends.tools.jid import JID
35 from sat.tools import config 38 from sat.tools import config
36 from sat.tools.common import dynamic_import 39 from sat.tools.common import dynamic_import
39 from sat.core import exceptions 42 from sat.core import exceptions
40 import sat_frontends.jp 43 import sat_frontends.jp
41 from sat_frontends.jp.constants import Const as C 44 from sat_frontends.jp.constants import Const as C
42 from sat_frontends.tools import misc 45 from sat_frontends.tools import misc
43 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI 46 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
44 import shlex
45 from collections import OrderedDict 47 from collections import OrderedDict
46 48
47 ## bridge handling 49 ## bridge handling
48 # we get bridge name from conf and initialise the right class accordingly 50 # we get bridge name from conf and initialise the right class accordingly
49 main_config = config.parseMainConf() 51 main_config = config.parseMainConf()
50 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') 52 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus')
53 USER_INTER_MSG = _("User interruption: good bye")
54
55
56 class QuitException(BaseException):
57 """Quitting is requested
58
59 This is used to stop execution when host.quit() is called
60 """
51 61
52 62
53 # TODO: move loops handling in a separated module 63 # TODO: move loops handling in a separated module
54 if 'dbus' in bridge_name: 64 if 'dbus' in bridge_name:
55 from gi.repository import GLib 65 from gi.repository import GLib
61 self.loop = GLib.MainLoop() 71 self.loop = GLib.MainLoop()
62 72
63 def run(self): 73 def run(self):
64 self.loop.run() 74 self.loop.run()
65 75
66 def quit(self): 76 def quit(self, exit_code):
67 self.loop.quit() 77 self.loop.quit()
78 sys.exit(exit_code)
68 79
69 def call_later(self, delay, callback, *args): 80 def call_later(self, delay, callback, *args):
70 """call a callback repeatedly 81 """call a callback repeatedly
71 82
72 @param delay(int): delay between calls in ms 83 @param delay(int): delay between calls in ms
76 @param *args: args of the callbac 87 @param *args: args of the callbac
77 """ 88 """
78 GLib.timeout_add(delay, callback, *args) 89 GLib.timeout_add(delay, callback, *args)
79 90
80 else: 91 else:
81 print("can't start jp: only D-Bus bridge is currently handled") 92 import signal
82 sys.exit(C.EXIT_ERROR) 93 from twisted.internet import asyncioreactor
83 # FIXME: twisted loop can be used when jp can handle fully async bridges 94 asyncioreactor.install()
84 # from twisted.internet import reactor 95 from twisted.internet import reactor, defer
85 96
86 # class JPLoop(object): 97 class JPLoop(object):
87 98
88 # def run(self): 99 def __init__(self):
89 # reactor.run() 100 # exit code must be set when using quit, so if it's not set
90 101 # something got wrong and we must report it
91 # def quit(self): 102 self._exit_code = C.EXIT_INTERNAL_ERROR
92 # reactor.stop() 103
93 104 def run(self, jp, *args):
94 # def _timeout_cb(self, args, callback, delay): 105 self.jp = jp
95 # ret = callback(*args) 106 signal.signal(signal.SIGINT, self._on_sigint)
96 # if ret: 107 defer.ensureDeferred(self._start(jp, *args))
97 # reactor.callLater(delay, self._timeout_cb, args, callback, delay) 108 try:
98 109 reactor.run(installSignalHandlers=False)
99 # def call_later(self, delay, callback, *args): 110 except SystemExit as e:
100 # delay = float(delay) / 1000 111 self._exit_code = e.code
101 # reactor.callLater(delay, self._timeout_cb, args, callback, delay) 112 sys.exit(self._exit_code)
113
114 async def _start(self, jp, *args):
115 fut = asyncio.ensure_future(jp.main(*args))
116 try:
117 await defer.Deferred.fromFuture(fut)
118 except BaseException:
119 import traceback
120 traceback.print_exc()
121 jp.quit(1)
122
123 def quit(self, exit_code):
124 self._exit_code = exit_code
125 reactor.stop()
126
127 def _timeout_cb(self, args, callback, delay):
128 try:
129 ret = callback(*args)
130 # FIXME: temporary hack to avoid traceback when using XMLUI
131 # to be removed once create_task is not used anymore in
132 # xmlui_manager (i.e. once sat_frontends.tools.xmlui fully supports
133 # async syntax)
134 except QuitException:
135 return
136 if ret:
137 reactor.callLater(delay, self._timeout_cb, args, callback, delay)
138
139 def call_later(self, delay, callback, *args):
140 delay = float(delay) / 1000
141 reactor.callLater(delay, self._timeout_cb, args, callback, delay)
142
143 def _on_sigint(self, sig_number, stack_frame):
144 """Called on keyboard interruption
145
146 Print user interruption message, set exit code and stop reactor
147 """
148 print("\r" + USER_INTER_MSG)
149 self._exit_code = C.EXIT_USER_CANCELLED
150 reactor.callFromThread(reactor.stop)
151
102 152
103 if bridge_name == "embedded": 153 if bridge_name == "embedded":
104 from sat.core import sat_main 154 from sat.core import sat_main
105 sat = sat_main.SAT() 155 sat = sat_main.SAT()
106 156
107 try: 157 try:
108 import progressbar 158 import progressbar
109 except ImportError: 159 except ImportError:
110 msg = (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') + 160 msg = (_('ProgressBar not available, please download it at '
111 _('Progress bar deactivated\n--\n')) 161 'http://pypi.python.org/pypi/progressbar\n'
112 print(msg.encode('utf-8'), file=sys.stderr) 162 'Progress bar deactivated\n--\n'))
163 print(msg, file=sys.stderr)
113 progressbar=None 164 progressbar=None
114 165
115 #consts 166 #consts
116 PROG_NAME = "jp" 167 PROG_NAME = "jp"
117 DESCRIPTION = """This software is a command line tool for XMPP. 168 DESCRIPTION = """This software is a command line tool for XMPP.
120 COPYLEFT = """Copyright (C) 2009-2019 Jérôme Poisson, Adrien Cossa 171 COPYLEFT = """Copyright (C) 2009-2019 Jérôme Poisson, Adrien Cossa
121 This program comes with ABSOLUTELY NO WARRANTY; 172 This program comes with ABSOLUTELY NO WARRANTY;
122 This is free software, and you are welcome to redistribute it under certain conditions. 173 This is free software, and you are welcome to redistribute it under certain conditions.
123 """ 174 """
124 175
125 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms 176 PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s
126 177
127 178
128 def date_decoder(arg): 179 def date_decoder(arg):
129 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) 180 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
130 181
139 190
140 """ 191 """
141 def __init__(self): 192 def __init__(self):
142 """ 193 """
143 194
144 @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting, 195 @attribute quit_on_progress_end (bool): set to False if you manage yourself
145 or if you want the user to stop by himself 196 exiting, or if you want the user to stop by himself
146 @attribute progress_success(callable): method to call when progress just started 197 @attribute progress_success(callable): method to call when progress just started
147 by default display a message 198 by default display a message
148 @attribute progress_success(callable): method to call when progress is successfully finished 199 @attribute progress_success(callable): method to call when progress is
149 by default display a message 200 successfully finished by default display a message
150 @attribute progress_failure(callable): method to call when progress failed 201 @attribute progress_failure(callable): method to call when progress failed
151 by default display a message 202 by default display a message
152 """ 203 """
153 # FIXME: need_loop should be removed, everything must be async in bridge so
154 # loop will always be needed
155 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') 204 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
156 if bridge_module is None: 205 if bridge_module is None:
157 log.error("Can't import {} bridge".format(bridge_name)) 206 log.error("Can't import {} bridge".format(bridge_name))
158 sys.exit(1) 207 sys.exit(1)
159 208
160 self.bridge = bridge_module.Bridge() 209 self.bridge = bridge_module.AIOBridge()
161 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) 210 self._onQuitCallbacks = []
162 211
163 def _bridgeCb(self): 212 def _bridgeConnected(self):
164 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, 213 self.parser = argparse.ArgumentParser(
165 description=DESCRIPTION) 214 formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION)
166 self._make_parents() 215 self._make_parents()
167 self.add_parser_options() 216 self.add_parser_options()
168 self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name') 217 self.subparsers = self.parser.add_subparsers(
169 self._auto_loop = False # when loop is used for internal reasons 218 title=_('Available commands'), dest='command', required=True)
170 self._need_loop = False
171 219
172 # progress attributes 220 # progress attributes
173 self._progress_id = None # TODO: manage several progress ids 221 self._progress_id = None # TODO: manage several progress ids
174 self.quit_on_progress_end = True 222 self.quit_on_progress_end = True
175 223
179 self._outputs[type_] = OrderedDict() 227 self._outputs[type_] = OrderedDict()
180 self.default_output = {} 228 self.default_output = {}
181 229
182 self.own_jid = None # must be filled at runtime if needed 230 self.own_jid = None # must be filled at runtime if needed
183 231
184 def _bridgeEb(self, failure):
185 if isinstance(failure, exceptions.BridgeExceptionNoService):
186 print((_("Can't connect to SàT backend, are you sure it's launched ?")))
187 elif isinstance(failure, exceptions.BridgeInitError):
188 print((_("Can't init bridge")))
189 else:
190 print((_("Error while initialising bridge: {}".format(failure))))
191 sys.exit(C.EXIT_BRIDGE_ERROR)
192
193 @property
194 def version(self):
195 return self.bridge.getVersion()
196
197 @property 232 @property
198 def progress_id(self): 233 def progress_id(self):
199 return self._progress_id 234 return self._progress_id
200 235
201 @progress_id.setter 236 async def set_progress_id(self, progress_id):
202 def progress_id(self, value): 237 # because we use async, we need an explicit setter
203 self._progress_id = value 238 self._progress_id = progress_id
204 self.replayCache('progress_ids_cache') 239 await self.replayCache('progress_ids_cache')
205 240
206 @property 241 @property
207 def watch_progress(self): 242 def watch_progress(self):
208 try: 243 try:
209 self.pbar 244 self.pbar
222 try: 257 try:
223 return self.args.verbose 258 return self.args.verbose
224 except AttributeError: 259 except AttributeError:
225 return 0 260 return 0
226 261
227 def replayCache(self, cache_attribute): 262 async def replayCache(self, cache_attribute):
228 """Replay cached signals 263 """Replay cached signals
229 264
230 @param cache_attribute(str): name of the attribute containing the cache 265 @param cache_attribute(str): name of the attribute containing the cache
231 if the attribute doesn't exist, there is no cache and the call is ignored 266 if the attribute doesn't exist, there is no cache and the call is ignored
232 else the cache must be a list of tuples containing the replay callback as first item, 267 else the cache must be a list of tuples containing the replay callback as
233 then the arguments to use 268 first item, then the arguments to use
234 """ 269 """
235 try: 270 try:
236 cache = getattr(self, cache_attribute) 271 cache = getattr(self, cache_attribute)
237 except AttributeError: 272 except AttributeError:
238 pass 273 pass
239 else: 274 else:
240 for cache_data in cache: 275 for cache_data in cache:
241 cache_data[0](*cache_data[1:]) 276 await cache_data[0](*cache_data[1:])
242 277
243 def disp(self, msg, verbosity=0, error=False, no_lf=False): 278 def disp(self, msg, verbosity=0, error=False, no_lf=False):
244 """Print a message to user 279 """Print a message to user
245 280
246 @param msg(unicode): message to print 281 @param msg(unicode): message to print
258 if no_lf: 293 if no_lf:
259 print(msg, end=' ') 294 print(msg, end=' ')
260 else: 295 else:
261 print(msg) 296 print(msg)
262 297
263 def output(self, type_, name, extra_outputs, data): 298 async def output(self, type_, name, extra_outputs, data):
264 if name in extra_outputs: 299 if name in extra_outputs:
265 extra_outputs[name](data) 300 method = extra_outputs[name]
266 else: 301 else:
267 self._outputs[type_][name]['callback'](data) 302 method = self._outputs[type_][name]['callback']
303
304 ret = method(data)
305 if inspect.isawaitable(ret):
306 await ret
268 307
269 def addOnQuitCallback(self, callback, *args, **kwargs): 308 def addOnQuitCallback(self, callback, *args, **kwargs):
270 """Add a callback which will be called on quit command 309 """Add a callback which will be called on quit command
271 310
272 @param callback(callback): method to call 311 @param callback(callback): method to call
273 """ 312 """
274 try: 313 self._onQuitCallbacks.append((callback, args, kwargs))
275 callbacks_list = self._onQuitCallbacks
276 except AttributeError:
277 callbacks_list = self._onQuitCallbacks = []
278 finally:
279 callbacks_list.append((callback, args, kwargs))
280 314
281 def getOutputChoices(self, output_type): 315 def getOutputChoices(self, output_type):
282 """Return valid output filters for output_type 316 """Return valid output filters for output_type
283 317
284 @param output_type: True for default, 318 @param output_type: True for default,
287 return list(self._outputs[output_type].keys()) 321 return list(self._outputs[output_type].keys())
288 322
289 def _make_parents(self): 323 def _make_parents(self):
290 self.parents = {} 324 self.parents = {}
291 325
292 # we have a special case here as the start-session option is present only if connection is not needed, 326 # we have a special case here as the start-session option is present only if
293 # so we create two similar parents, one with the option, the other one without it 327 # connection is not needed, so we create two similar parents, one with the
328 # option, the other one without it
294 for parent_name in ('profile', 'profile_session'): 329 for parent_name in ('profile', 'profile_session'):
295 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) 330 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
296 parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) 331 parent.add_argument(
297 parent.add_argument("--pwd", action="store", default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) 332 "-p", "--profile", action="store", type=str, default='@DEFAULT@',
298 333 help=_("Use PROFILE profile key (default: %(default)s)"))
299 profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] 334 parent.add_argument(
300 335 "--pwd", action="store", default='', metavar='PASSWORD',
301 connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _("Connect the profile before doing anything else") 336 help=_("Password used to connect profile, if necessary"))
302 profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) 337
338 profile_parent, profile_session_parent = (self.parents['profile'],
339 self.parents['profile_session'])
340
341 connect_short, connect_long, connect_action, connect_help = (
342 "-c", "--connect", "store_true",
343 _("Connect the profile before doing anything else")
344 )
345 profile_parent.add_argument(
346 connect_short, connect_long, action=connect_action, help=connect_help)
303 347
304 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() 348 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group()
305 profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) 349 profile_session_connect_group.add_argument(
306 profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) 350 connect_short, connect_long, action=connect_action, help=connect_help)
307 351 profile_session_connect_group.add_argument(
308 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) 352 "--start-session", action="store_true",
353 help=_("Start a profile session without connecting"))
354
355 progress_parent = self.parents['progress'] = argparse.ArgumentParser(
356 add_help=False)
309 if progressbar: 357 if progressbar:
310 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) 358 progress_parent.add_argument(
359 "-P", "--progress", action="store_true", help=_("Show progress bar"))
311 360
312 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) 361 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
313 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_("Add a verbosity level (can be used multiple times)")) 362 verbose_parent.add_argument(
363 '--verbose', '-v', action='count', default=0,
364 help=_("Add a verbosity level (can be used multiple times)"))
314 365
315 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) 366 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
316 draft_group = draft_parent.add_argument_group(_('draft handling')) 367 draft_group = draft_parent.add_argument_group(_('draft handling'))
317 draft_group.add_argument("-D", "--current", action="store_true", help=_("load current draft")) 368 draft_group.add_argument(
318 draft_group.add_argument("-F", "--draft-path", help=_("path to a draft file to retrieve")) 369 "-D", "--current", action="store_true", help=_("load current draft"))
370 draft_group.add_argument(
371 "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve"))
319 372
320 373
321 def make_pubsub_group(self, flags, defaults): 374 def make_pubsub_group(self, flags, defaults):
322 """generate pubsub options according to flags 375 """generate pubsub options according to flags
323 376
354 default = defaults.pop('item', _('last item')) 407 default = defaults.pop('item', _('last item'))
355 if default is not None: 408 if default is not None:
356 item_help += _(" (DEFAULT: {default})".format(default=default)) 409 item_help += _(" (DEFAULT: {default})".format(default=default))
357 pubsub_group.add_argument("-i", "--item", default='', 410 pubsub_group.add_argument("-i", "--item", default='',
358 help=item_help) 411 help=item_help)
359 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_('retrieve last item')) 412 pubsub_group.add_argument(
413 "-L", "--last-item", action='store_true', help=_('retrieve last item'))
360 elif flags.multi_items: 414 elif flags.multi_items:
361 # mutiple items, this activate several features: max-items, RSM, MAM 415 # mutiple items, this activate several features: max-items, RSM, MAM
362 # and Orbder-by 416 # and Orbder-by
363 pubsub_group.add_argument("-i", "--item", action='append', dest='items', default=[], help=_("items to retrieve (DEFAULT: all)")) 417 pubsub_group.add_argument(
418 "-i", "--item", action='append', dest='items', default=[],
419 help=_("items to retrieve (DEFAULT: all)"))
364 if not flags.no_max: 420 if not flags.no_max:
365 max_group = pubsub_group.add_mutually_exclusive_group() 421 max_group = pubsub_group.add_mutually_exclusive_group()
366 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args 422 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args
367 max_group.add_argument( 423 max_group.add_argument(
368 "-M", "--max-items", dest="max", type=int, 424 "-M", "--max-items", dest="max", type=int,
407 "-o", "--order-by", choices=[C.ORDER_BY_CREATION, 463 "-o", "--order-by", choices=[C.ORDER_BY_CREATION,
408 C.ORDER_BY_MODIFICATION], 464 C.ORDER_BY_MODIFICATION],
409 help=_("how items should be ordered")) 465 help=_("how items should be ordered"))
410 466
411 if not flags.all_used: 467 if not flags.all_used:
412 raise exceptions.InternalError('unknown flags: {flags}'.format(flags=', '.join(flags.unused))) 468 raise exceptions.InternalError('unknown flags: {flags}'.format(
469 flags=', '.join(flags.unused)))
413 if defaults: 470 if defaults:
414 raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults)) 471 raise exceptions.InternalError(f'unused defaults: {defaults}')
415 472
416 return parent 473 return parent
417 474
418 def add_parser_options(self): 475 def add_parser_options(self):
419 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) 476 self.parser.add_argument(
477 '--version',
478 action='version',
479 version=("{name} {version} {copyleft}".format(
480 name = PROG_NAME,
481 version = self.version,
482 copyleft = COPYLEFT))
483 )
420 484
421 def register_output(self, type_, name, callback, description="", default=False): 485 def register_output(self, type_, name, callback, description="", default=False):
422 if type_ not in C.OUTPUT_TYPES: 486 if type_ not in C.OUTPUT_TYPES:
423 log.error("Invalid output type {}".format(type_)) 487 log.error("Invalid output type {}".format(type_))
424 return 488 return
425 self._outputs[type_][name] = {'callback': callback, 489 self._outputs[type_][name] = {'callback': callback,
426 'description': description 490 'description': description
427 } 491 }
428 if default: 492 if default:
429 if type_ in self.default_output: 493 if type_ in self.default_output:
430 self.disp(_('there is already a default output for {}, ignoring new one').format(type_)) 494 self.disp(
495 _(f'there is already a default output for {type_}, ignoring new one')
496 )
431 else: 497 else:
432 self.default_output[type_] = name 498 self.default_output[type_] = name
433 499
434 500
435 def parse_output_options(self): 501 def parse_output_options(self):
443 options_dict[key.strip()] = value.strip() if value is not None else None 509 options_dict[key.strip()] = value.strip() if value is not None else None
444 return options_dict 510 return options_dict
445 511
446 def check_output_options(self, accepted_set, options): 512 def check_output_options(self, accepted_set, options):
447 if not accepted_set.issuperset(options): 513 if not accepted_set.issuperset(options):
448 self.disp("The following output options are invalid: {invalid_options}".format( 514 self.disp(
515 _("The following output options are invalid: {invalid_options}").format(
449 invalid_options = ', '.join(set(options).difference(accepted_set))), 516 invalid_options = ', '.join(set(options).difference(accepted_set))),
450 error=True) 517 error=True)
451 self.quit(C.EXIT_BAD_ARG) 518 self.quit(C.EXIT_BAD_ARG)
452 519
453 def import_plugins(self): 520 def import_plugins(self):
455 522
456 looks from modules names cmd_*.py in jp path and import them 523 looks from modules names cmd_*.py in jp path and import them
457 """ 524 """
458 path = os.path.dirname(sat_frontends.jp.__file__) 525 path = os.path.dirname(sat_frontends.jp.__file__)
459 # XXX: outputs must be imported before commands as they are used for arguments 526 # XXX: outputs must be imported before commands as they are used for arguments
460 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')): 527 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'),
461 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) 528 (C.PLUGIN_CMD, 'cmd_*.py')):
529 modules = (
530 os.path.splitext(module)[0]
531 for module in map(os.path.basename, iglob(os.path.join(path, pattern))))
462 for module_name in modules: 532 for module_name in modules:
463 module_path = "sat_frontends.jp." + module_name 533 module_path = "sat_frontends.jp." + module_name
464 try: 534 try:
465 module = import_module(module_path) 535 module = import_module(module_path)
466 self.import_plugin_module(module, type_) 536 self.import_plugin_module(module, type_)
467 except ImportError as e: 537 except ImportError as e:
468 self.disp(_("Can't import {module_path} plugin, ignoring it: {msg}".format( 538 self.disp(
469 module_path = module_path, 539 _(f"Can't import {module_path} plugin, ignoring it: {e}"),
470 msg = e)), error=True) 540 error=True)
471 except exceptions.CancelError: 541 except exceptions.CancelError:
472 continue 542 continue
473 except exceptions.MissingModule as e: 543 except exceptions.MissingModule as e:
474 self.disp(_("Missing module for plugin {name}: {missing}".format( 544 self.disp(_("Missing module for plugin {name}: {missing}".format(
475 name = module_path, 545 name = module_path,
483 @param type_(str): one of C_PLUGIN_* 553 @param type_(str): one of C_PLUGIN_*
484 """ 554 """
485 try: 555 try:
486 class_names = getattr(module, '__{}__'.format(type_)) 556 class_names = getattr(module, '__{}__'.format(type_))
487 except AttributeError: 557 except AttributeError:
488 log.disp(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True) 558 log.disp(_(f"Invalid plugin module [{type_}] {module}"), error=True)
489 raise ImportError 559 raise ImportError
490 else: 560 else:
491 for class_name in class_names: 561 for class_name in class_names:
492 cls = getattr(module, class_name) 562 cls = getattr(module, class_name)
493 cls(self) 563 cls(self)
498 scheme = 'https' 568 scheme = 'https'
499 elif http_url.startswith('http'): 569 elif http_url.startswith('http'):
500 scheme = 'http' 570 scheme = 'http'
501 else: 571 else:
502 raise exceptions.InternalError('An HTTP scheme is expected in this method') 572 raise exceptions.InternalError('An HTTP scheme is expected in this method')
503 self.disp("{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1) 573 self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1)
504 # HTTP URL, we try to find xmpp: links 574 # HTTP URL, we try to find xmpp: links
505 try: 575 try:
506 from lxml import etree 576 from lxml import etree
507 except ImportError: 577 except ImportError:
508 self.disp("lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) 578 self.disp(
579 "lxml module must be installed to use http(s) scheme, please install it "
580 "with \"pip install lxml\"",
581 error=True)
509 self.quit(1) 582 self.quit(1)
510 import urllib.request, urllib.error, urllib.parse 583 import urllib.request, urllib.error, urllib.parse
511 parser = etree.HTMLParser() 584 parser = etree.HTMLParser()
512 try: 585 try:
513 root = etree.parse(urllib.request.urlopen(http_url), parser) 586 root = etree.parse(urllib.request.urlopen(http_url), parser)
515 self.disp(_("Can't parse HTML page : {msg}").format(msg=e)) 588 self.disp(_("Can't parse HTML page : {msg}").format(msg=e))
516 links = [] 589 links = []
517 else: 590 else:
518 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") 591 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
519 if not links: 592 if not links:
520 self.disp('Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) 593 self.disp(
594 _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP '
595 'PubSub node/item'),
596 error=True)
521 self.quit(1) 597 self.quit(1)
522 xmpp_uri = links[0].get('href') 598 xmpp_uri = links[0].get('href')
523 return xmpp_uri 599 return xmpp_uri
524 600
525 def parse_pubsub_args(self): 601 def parse_pubsub_args(self):
550 item = self.args.item 626 item = self.args.item
551 except AttributeError: 627 except AttributeError:
552 try: 628 try:
553 items = self.args.items 629 items = self.args.items
554 except AttributeError: 630 except AttributeError:
555 self.disp(_("item specified in URL but not needed in command, ignoring it"), error=True) 631 self.disp(
632 _("item specified in URL but not needed in command, "
633 "ignoring it"),
634 error=True)
556 else: 635 else:
557 if not items: 636 if not items:
558 self.args.items = [uri_item] 637 self.args.items = [uri_item]
559 else: 638 else:
560 if not item: 639 if not item:
563 except AttributeError: 642 except AttributeError:
564 item_last = False 643 item_last = False
565 if not item_last: 644 if not item_last:
566 self.args.item = uri_item 645 self.args.item = uri_item
567 else: 646 else:
568 self.parser.error(_('XMPP URL is not a pubsub one: {url}').format(url=url)) 647 self.parser.error(_(f'XMPP URL is not a pubsub one: {url}'))
569 flags = self.args._cmd._pubsub_flags 648 flags = self.args._cmd._pubsub_flags
570 # we check required arguments here instead of using add_arguments' required option 649 # we check required arguments here instead of using add_arguments' required option
571 # because the required argument can be set in URL 650 # because the required argument can be set in URL
572 if C.SERVICE in flags and not self.args.service: 651 if C.SERVICE in flags and not self.args.service:
573 self.parser.error(_("argument -s/--service is required")) 652 self.parser.error(_("argument -s/--service is required"))
578 657
579 # FIXME: mutually groups can't be nested in a group and don't support title 658 # FIXME: mutually groups can't be nested in a group and don't support title
580 # so we check conflict here. This may be fixed in Python 3, to be checked 659 # so we check conflict here. This may be fixed in Python 3, to be checked
581 try: 660 try:
582 if self.args.item and self.args.item_last: 661 if self.args.item and self.args.item_last:
583 self.parser.error(_("--item and --item-last can't be used at the same time")) 662 self.parser.error(
663 _("--item and --item-last can't be used at the same time"))
584 except AttributeError: 664 except AttributeError:
585 pass 665 pass
586 666
587 try: 667 try:
588 max_items = self.args.max 668 max_items = self.args.max
603 # we use pubsub without RSM 683 # we use pubsub without RSM
604 self.args.max = 10 684 self.args.max = 10
605 if self.args.max is None: 685 if self.args.max is None:
606 self.args.max = C.NO_LIMIT 686 self.args.max = C.NO_LIMIT
607 687
688 async def main(self, args, namespace):
689 try:
690 await self.bridge.bridgeConnect()
691 except Exception as e:
692 if isinstance(e, exceptions.BridgeExceptionNoService):
693 print((_("Can't connect to SàT backend, are you sure it's launched ?")))
694 elif isinstance(e, exceptions.BridgeInitError):
695 print((_("Can't init bridge")))
696 else:
697 print((_(f"Error while initialising bridge: {e}")))
698 self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
699 return
700 self.version = await self.bridge.getVersion()
701 self._bridgeConnected()
702 self.import_plugins()
703 try:
704 self.args = self.parser.parse_args(args, namespace=None)
705 if self.args._cmd._use_pubsub:
706 self.parse_pubsub_args()
707 await self.args._cmd.run()
708 except SystemExit as e:
709 self.quit(e.code, raise_exc=False)
710 return
711 except QuitException:
712 return
713
608 def run(self, args=None, namespace=None): 714 def run(self, args=None, namespace=None):
609 self.args = self.parser.parse_args(args, namespace=None)
610 if self.args._cmd._use_pubsub:
611 self.parse_pubsub_args()
612 try:
613 self.args._cmd.run()
614 if self._need_loop or self._auto_loop:
615 self._start_loop()
616 except KeyboardInterrupt:
617 self.disp(_("User interruption: good bye"))
618
619 def _start_loop(self):
620 self.loop = JPLoop() 715 self.loop = JPLoop()
621 self.loop.run() 716 self.loop.run(self, args, namespace)
622 717
623 def stop_loop(self): 718 def _read_stdin(self, stdin_fut):
624 try: 719 """Callback called by ainput to read stdin"""
625 self.loop.quit() 720 line = sys.stdin.readline()
626 except AttributeError: 721 if line:
627 pass 722 stdin_fut.set_result(line.rstrip(os.linesep))
628 723 else:
629 def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")): 724 stdin_fut.set_exception(EOFError())
725
726 async def ainput(self, msg=''):
727 """Asynchronous version of buildin "input" function"""
728 self.disp(msg, no_lf=True)
729 sys.stdout.flush()
730 loop = asyncio.get_running_loop()
731 stdin_fut = loop.create_future()
732 loop.add_reader(sys.stdin, self._read_stdin, stdin_fut)
733 return await stdin_fut
734
735 async def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")):
630 """Request user to confirm action, and quit if he doesn't""" 736 """Request user to confirm action, and quit if he doesn't"""
631 737
632 res = input("{} (y/N)? ".format(message)) 738 res = await self.ainput(f"{message} (y/N)? ")
633 if res not in ("y", "Y"): 739 if res not in ("y", "Y"):
634 self.disp(cancel_message) 740 self.disp(cancel_message)
635 self.quit(C.EXIT_USER_CANCELLED) 741 self.quit(C.EXIT_USER_CANCELLED)
636 742
637 def quitFromSignal(self, errcode=0): 743 def quitFromSignal(self, exit_code=0):
638 """Same as self.quit, but from a signal handler 744 r"""Same as self.quit, but from a signal handler
639 745
640 /!\: return must be used after calling this method ! 746 /!\: return must be used after calling this method !
641 """ 747 """
642 assert self._need_loop
643 # XXX: python-dbus will show a traceback if we exit in a signal handler 748 # XXX: python-dbus will show a traceback if we exit in a signal handler
644 # so we use this little timeout trick to avoid it 749 # so we use this little timeout trick to avoid it
645 self.loop.call_later(0, self.quit, errcode) 750 self.loop.call_later(0, self.quit, exit_code)
646 751
647 def quit(self, errcode=0): 752 def quit(self, exit_code=0, raise_exc=True):
753 """Terminate the execution with specified exit_code
754
755 This will stop the loop.
756 @param exit_code(int): code to return when quitting the program
757 @param raise_exp(boolean): if True raise a QuitException to stop code execution
758 The default value should be used most of time.
759 """
648 # first the onQuitCallbacks 760 # first the onQuitCallbacks
649 try: 761 try:
650 callbacks_list = self._onQuitCallbacks 762 callbacks_list = self._onQuitCallbacks
651 except AttributeError: 763 except AttributeError:
652 pass 764 pass
653 else: 765 else:
654 for callback, args, kwargs in callbacks_list: 766 for callback, args, kwargs in callbacks_list:
655 callback(*args, **kwargs) 767 callback(*args, **kwargs)
656 768
657 self.stop_loop() 769 self.loop.quit(exit_code)
658 sys.exit(errcode) 770 if raise_exc:
659 771 raise QuitException
660 def check_jids(self, jids): 772
773 async def check_jids(self, jids):
661 """Check jids validity, transform roster name to corresponding jids 774 """Check jids validity, transform roster name to corresponding jids
662 775
663 @param profile: profile name 776 @param profile: profile name
664 @param jids: list of jids 777 @param jids: list of jids
665 @return: List of jids 778 @return: List of jids
666 779
667 """ 780 """
668 names2jid = {} 781 names2jid = {}
669 nodes2jid = {} 782 nodes2jid = {}
670 783
671 for contact in self.bridge.getContacts(self.profile): 784 for contact in await self.bridge.getContacts(self.profile):
672 jid_s, attr, groups = contact 785 jid_s, attr, groups = contact
673 _jid = JID(jid_s) 786 _jid = JID(jid_s)
674 try: 787 try:
675 names2jid[attr["name"].lower()] = jid_s 788 names2jid[attr["name"].lower()] = jid_s
676 except KeyError: 789 except KeyError:
702 except AttributeError: 815 except AttributeError:
703 pass 816 pass
704 817
705 return dest_jids 818 return dest_jids
706 819
707 def connect_profile(self, callback): 820 async def connect_profile(self):
708 """ Check if the profile is connected and do it if requested 821 """Check if the profile is connected and do it if requested
709 822
710 @param callback: method to call when profile is connected
711 @exit: - 1 when profile is not connected and --connect is not set 823 @exit: - 1 when profile is not connected and --connect is not set
712 - 1 when the profile doesn't exists 824 - 1 when the profile doesn't exists
713 - 1 when there is a connection error 825 - 1 when there is a connection error
714 """ 826 """
715 # FIXME: need better exit codes 827 # FIXME: need better exit codes
716 828
717 def cant_connect(failure): 829 self.profile = await self.bridge.profileNameGet(self.args.profile)
718 log.error(_("Can't connect profile: {reason}").format(reason=failure))
719 self.quit(1)
720
721 def cant_start_session(failure):
722 log.error(_("Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure))
723 self.quit(1)
724
725 self.profile = self.bridge.profileNameGet(self.args.profile)
726 830
727 if not self.profile: 831 if not self.profile:
728 log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile)) 832 log.error(_(f"The profile [{self.args.profile}] doesn't exist"))
729 self.quit(1) 833 self.quit(C.EXIT_ERROR)
730 834
731 try: 835 try:
732 start_session = self.args.start_session 836 start_session = self.args.start_session
733 except AttributeError: 837 except AttributeError:
734 pass 838 pass
735 else: 839 else:
736 if start_session: 840 if start_session:
737 self.bridge.profileStartSession(self.args.pwd, self.profile, lambda __: callback(), cant_start_session) 841 try:
738 self._auto_loop = True 842 await self.bridge.profileStartSession(self.args.pwd, self.profile)
843 except Exception as e:
844 self.disp(_(f"Can't start {self.profile}'s session: {e}"), err=True)
845 self.quit(1)
739 return 846 return
740 elif not self.bridge.profileIsSessionStarted(self.profile): 847 elif not await self.bridge.profileIsSessionStarted(self.profile):
741 if not self.args.connect: 848 if not self.args.connect:
742 log.error(_("Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile)) 849 self.disp(_(
850 f"Session for [{self.profile}] is not started, please start it "
851 f"before using jp, or use either --start-session or --connect "
852 f"option"), error=True)
743 self.quit(1) 853 self.quit(1)
744 elif not getattr(self.args, "connect", False): 854 elif not getattr(self.args, "connect", False):
745 callback()
746 return 855 return
747 856
748 857
749 if not hasattr(self.args, 'connect'): 858 if not hasattr(self.args, 'connect'):
750 # a profile can be present without connect option (e.g. on profile creation/deletion) 859 # a profile can be present without connect option (e.g. on profile
860 # creation/deletion)
751 return 861 return
752 elif self.args.connect is True: # if connection is asked, we connect the profile 862 elif self.args.connect is True: # if connection is asked, we connect the profile
753 self.bridge.connect(self.profile, self.args.pwd, {}, lambda __: callback(), cant_connect) 863 try:
754 self._auto_loop = True 864 await self.bridge.connect(self.profile, self.args.pwd, {})
865 except Exception as e:
866 self.disp(_(f"Can't connect profile: {e}"), error=True)
867 self.quit(1)
755 return 868 return
756 else: 869 else:
757 if not self.bridge.isConnected(self.profile): 870 if not await self.bridge.isConnected(self.profile):
758 log.error(_("Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) 871 log.error(
872 _(f"Profile [{self.profile}] is not connected, please connect it "
873 f"before using jp, or use --connect option"))
759 self.quit(1) 874 self.quit(1)
760 875
761 callback() 876 async def get_full_jid(self, param_jid):
762
763 def get_full_jid(self, param_jid):
764 """Return the full jid if possible (add main resource when find a bare jid)""" 877 """Return the full jid if possible (add main resource when find a bare jid)"""
765 _jid = JID(param_jid) 878 _jid = JID(param_jid)
766 if not _jid.resource: 879 if not _jid.resource:
767 #if the resource is not given, we try to add the main resource 880 #if the resource is not given, we try to add the main resource
768 main_resource = self.bridge.getMainResource(param_jid, self.profile) 881 main_resource = await self.bridge.getMainResource(param_jid, self.profile)
769 if main_resource: 882 if main_resource:
770 return "%s/%s" % (_jid.bare, main_resource) 883 return f"{_jid.bare}/{main_resource}"
771 return param_jid 884 return param_jid
772 885
773 886
774 class CommandBase(object): 887 class CommandBase(object):
775 888
781 @param name(unicode): name of the new command 894 @param name(unicode): name of the new command
782 @param use_profile(bool): if True, add profile selection/connection commands 895 @param use_profile(bool): if True, add profile selection/connection commands
783 @param use_output(bool, unicode): if not False, add --output option 896 @param use_output(bool, unicode): if not False, add --output option
784 @param extra_outputs(dict): list of command specific outputs: 897 @param extra_outputs(dict): list of command specific outputs:
785 key is output name ("default" to use as main output) 898 key is output name ("default" to use as main output)
786 value is a callable which will format the output (data will be used as only argument) 899 value is a callable which will format the output (data will be used as only
900 argument)
787 if a key already exists with normal outputs, the extra one will be used 901 if a key already exists with normal outputs, the extra one will be used
788 @param need_connect(bool, None): True if profile connection is needed 902 @param need_connect(bool, None): True if profile connection is needed
789 False else (profile session must still be started) 903 False else (profile session must still be started)
790 None to set auto value (i.e. True if use_profile is set) 904 None to set auto value (i.e. True if use_profile is set)
791 Can't be set if use_profile is False 905 Can't be set if use_profile is False
797 - use_verbose(bool): if True, add verbosity option 911 - use_verbose(bool): if True, add verbosity option
798 - use_pubsub(bool): if True, add pubsub options 912 - use_pubsub(bool): if True, add pubsub options
799 mandatory arguments are controlled by pubsub_req 913 mandatory arguments are controlled by pubsub_req
800 - use_draft(bool): if True, add draft handling options 914 - use_draft(bool): if True, add draft handling options
801 ** other arguments ** 915 ** other arguments **
802 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be: 916 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options,
917 can be:
803 C.SERVICE: service is required 918 C.SERVICE: service is required
804 C.NODE: node is required 919 C.NODE: node is required
805 C.ITEM: item is required 920 C.ITEM: item is required
806 C.SINGLE_ITEM: only one item is allowed 921 C.SINGLE_ITEM: only one item is allowed
807 @attribute need_loop(bool): to set by commands when loop is needed 922 """
808 """
809 self.need_loop = False # to be set by commands when loop is needed
810 try: # If we have subcommands, host is a CommandBase and we need to use host.host 923 try: # If we have subcommands, host is a CommandBase and we need to use host.host
811 self.host = host.host 924 self.host = host.host
812 except AttributeError: 925 except AttributeError:
813 self.host = host 926 self.host = host
814 927
815 # --profile option 928 # --profile option
816 parents = kwargs.setdefault('parents', set()) 929 parents = kwargs.setdefault('parents', set())
817 if use_profile: 930 if use_profile:
818 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments 931 # self.host.parents['profile'] is an ArgumentParser with profile connection
932 # arguments
819 if need_connect is None: 933 if need_connect is None:
820 need_connect = True 934 need_connect = True
821 parents.add(self.host.parents['profile' if need_connect else 'profile_session']) 935 parents.add(
936 self.host.parents['profile' if need_connect else 'profile_session'])
822 else: 937 else:
823 assert need_connect is None 938 assert need_connect is None
824 self.need_connect = need_connect 939 self.need_connect = need_connect
825 # from this point, self.need_connect is None if connection is not needed at all 940 # from this point, self.need_connect is None if connection is not needed at all
826 # False if session starting is needed, and True if full connection is needed 941 # False if session starting is needed, and True if full connection is needed
836 self._output_type = use_output 951 self._output_type = use_output
837 output_parent = argparse.ArgumentParser(add_help=False) 952 output_parent = argparse.ArgumentParser(add_help=False)
838 choices = set(self.host.getOutputChoices(use_output)) 953 choices = set(self.host.getOutputChoices(use_output))
839 choices.update(extra_outputs) 954 choices.update(extra_outputs)
840 if not choices: 955 if not choices:
841 raise exceptions.InternalError("No choice found for {} output type".format(use_output)) 956 raise exceptions.InternalError(
957 "No choice found for {} output type".format(use_output))
842 try: 958 try:
843 default = self.host.default_output[use_output] 959 default = self.host.default_output[use_output]
844 except KeyError: 960 except KeyError:
845 if 'default' in choices: 961 if 'default' in choices:
846 default = 'default' 962 default = 'default'
847 elif 'simple' in choices: 963 elif 'simple' in choices:
848 default = 'simple' 964 default = 'simple'
849 else: 965 else:
850 default = list(choices)[0] 966 default = list(choices)[0]
851 output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_("select output format (default: {})".format(default))) 967 output_parent.add_argument(
852 output_parent.add_argument('--output-option', '--oo', action="append", dest='output_opts', default=[], help=_("output specific option")) 968 '--output', '-O', choices=sorted(choices), default=default,
969 help=_("select output format (default: {})".format(default)))
970 output_parent.add_argument(
971 '--output-option', '--oo', action="append", dest='output_opts',
972 default=[], help=_("output specific option"))
853 parents.add(output_parent) 973 parents.add(output_parent)
854 else: 974 else:
855 assert extra_outputs is None 975 assert extra_outputs is None
856 976
857 self._use_pubsub = kwargs.pop('use_pubsub', False) 977 self._use_pubsub = kwargs.pop('use_pubsub', False)
871 if do_use: 991 if do_use:
872 parents.add(self.host.parents[opt]) 992 parents.add(self.host.parents[opt])
873 993
874 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) 994 self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
875 if hasattr(self, "subcommands"): 995 if hasattr(self, "subcommands"):
876 self.subparsers = self.parser.add_subparsers() 996 self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True)
877 else: 997 else:
878 self.parser.set_defaults(_cmd=self) 998 self.parser.set_defaults(_cmd=self)
879 self.add_parser_options() 999 self.add_parser_options()
880 1000
881 @property 1001 @property
892 1012
893 @property 1013 @property
894 def progress_id(self): 1014 def progress_id(self):
895 return self.host.progress_id 1015 return self.host.progress_id
896 1016
897 @progress_id.setter 1017 async def set_progress_id(self, progress_id):
898 def progress_id(self, value): 1018 return await self.host.set_progress_id(progress_id)
899 self.host.progress_id = value 1019
900 1020 async def progressStartedHandler(self, uid, metadata, profile):
901 def progressStartedHandler(self, uid, metadata, profile):
902 if profile != self.profile: 1021 if profile != self.profile:
903 return 1022 return
904 if self.progress_id is None: 1023 if self.progress_id is None:
905 # the progress started message can be received before the id 1024 # the progress started message can be received before the id
906 # so we keep progressStarted signals in cache to replay they 1025 # so we keep progressStarted signals in cache to replay they
907 # when the progress_id is received 1026 # when the progress_id is received
908 cache_data = (self.progressStartedHandler, uid, metadata, profile) 1027 cache_data = (self.progressStartedHandler, uid, metadata, profile)
909 try: 1028 try:
910 self.host.progress_ids_cache.append(cache_data) 1029 cache = self.host.progress_ids_cache
911 except AttributeError: 1030 except AttributeError:
912 self.host.progress_ids_cache = [cache_data] 1031 cache = self.host.progress_ids_cache = []
1032 cache.append(cache_data)
913 else: 1033 else:
914 if self.host.watch_progress and uid == self.progress_id: 1034 if self.host.watch_progress and uid == self.progress_id:
915 self.onProgressStarted(metadata) 1035 await self.onProgressStarted(metadata)
916 self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate) 1036 while True:
917 1037 await asyncio.sleep(PROGRESS_DELAY)
918 def progressFinishedHandler(self, uid, metadata, profile): 1038 cont = await self.progressUpdate()
1039 if not cont:
1040 break
1041
1042 async def progressFinishedHandler(self, uid, metadata, profile):
919 if profile != self.profile: 1043 if profile != self.profile:
920 return 1044 return
921 if uid == self.progress_id: 1045 if uid == self.progress_id:
922 try: 1046 try:
923 self.host.pbar.finish() 1047 self.host.pbar.finish()
924 except AttributeError: 1048 except AttributeError:
925 pass 1049 pass
926 self.onProgressFinished(metadata) 1050 await self.onProgressFinished(metadata)
927 if self.host.quit_on_progress_end: 1051 if self.host.quit_on_progress_end:
928 self.host.quitFromSignal() 1052 self.host.quitFromSignal()
929 1053
930 def progressErrorHandler(self, uid, message, profile): 1054 async def progressErrorHandler(self, uid, message, profile):
931 if profile != self.profile: 1055 if profile != self.profile:
932 return 1056 return
933 if uid == self.progress_id: 1057 if uid == self.progress_id:
934 if self.args.progress: 1058 if self.args.progress:
935 self.disp('') # progress is not finished, so we skip a line 1059 self.disp('') # progress is not finished, so we skip a line
936 if self.host.quit_on_progress_end: 1060 if self.host.quit_on_progress_end:
937 self.onProgressError(message) 1061 await self.onProgressError(message)
938 self.host.quitFromSignal(1) 1062 self.host.quitFromSignal(C.EXIT_ERROR)
939 1063
940 def progressUpdate(self): 1064 async def progressUpdate(self):
941 """This method is continualy called to update the progress bar""" 1065 """This method is continualy called to update the progress bar
942 data = self.host.bridge.progressGet(self.progress_id, self.profile) 1066
1067 @return (bool): False to stop being called
1068 """
1069 data = await self.host.bridge.progressGet(self.progress_id, self.profile)
943 if data: 1070 if data:
944 try: 1071 try:
945 size = data['size'] 1072 size = data['size']
946 except KeyError: 1073 except KeyError:
947 self.disp(_("file size is not known, we can't show a progress bar"), 1, error=True) 1074 self.disp(_("file size is not known, we can't show a progress bar"), 1,
1075 error=True)
948 return False 1076 return False
949 if self.host.pbar is None: 1077 if self.host.pbar is None:
950 #first answer, we must construct the bar 1078 #first answer, we must construct the bar
951 self.host.pbar = progressbar.ProgressBar(max_value=int(size), 1079
952 widgets=[_("Progress: "),progressbar.Percentage(), 1080 # if the instance has a pbar_template attribute, it is used has model,
953 " ", 1081 # else default one is used
954 progressbar.Bar(), 1082 # template is a list of part, where part can be either a str to show directly
955 " ", 1083 # or a list where first argument is a name of a progressbar widget, and others
956 progressbar.FileTransferSpeed(), 1084 # are used as widget arguments
957 " ", 1085 try:
958 progressbar.ETA()]) 1086 template = self.pbar_template
1087 except AttributeError:
1088 template = [
1089 _("Progress: "), ["Percentage"], " ", ["Bar"], " ",
1090 ["FileTransferSpeed"], " ", ["ETA"]
1091 ]
1092
1093 widgets = []
1094 for part in template:
1095 if isinstance(part, str):
1096 widgets.append(part)
1097 else:
1098 widget = getattr(progressbar, part.pop(0))
1099 widgets.append(widget(*part))
1100
1101 self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets)
959 self.host.pbar.start() 1102 self.host.pbar.start()
960 1103
961 self.host.pbar.update(int(data['position'])) 1104 self.host.pbar.update(int(data['position']))
962 1105
963 elif self.host.pbar is not None: 1106 elif self.host.pbar is not None:
964 return False 1107 return False
965 1108
966 self.onProgressUpdate(data) 1109 await self.onProgressUpdate(data)
967 1110
968 return True 1111 return True
969 1112
970 def onProgressStarted(self, metadata): 1113 async def onProgressStarted(self, metadata):
971 """Called when progress has just started 1114 """Called when progress has just started
972 1115
973 can be overidden by a command 1116 can be overidden by a command
974 @param metadata(dict): metadata as sent by bridge.progressStarted 1117 @param metadata(dict): metadata as sent by bridge.progressStarted
975 """ 1118 """
976 self.disp(_("Operation started"), 2) 1119 self.disp(_("Operation started"), 2)
977 1120
978 def onProgressUpdate(self, metadata): 1121 async def onProgressUpdate(self, metadata):
979 """Method called on each progress updata 1122 """Method called on each progress updata
980 1123
981 can be overidden by a command to handle progress metadata 1124 can be overidden by a command to handle progress metadata
982 @para metadata(dict): metadata as returned by bridge.progressGet 1125 @para metadata(dict): metadata as returned by bridge.progressGet
983 """ 1126 """
984 pass 1127 pass
985 1128
986 def onProgressFinished(self, metadata): 1129 async def onProgressFinished(self, metadata):
987 """Called when progress has just finished 1130 """Called when progress has just finished
988 1131
989 can be overidden by a command 1132 can be overidden by a command
990 @param metadata(dict): metadata as sent by bridge.progressFinished 1133 @param metadata(dict): metadata as sent by bridge.progressFinished
991 """ 1134 """
992 self.disp(_("Operation successfully finished"), 2) 1135 self.disp(_("Operation successfully finished"), 2)
993 1136
994 def onProgressError(self, error_msg): 1137 async def onProgressError(self, e):
995 """Called when a progress failed 1138 """Called when a progress failed
996 1139
997 @param error_msg(unicode): error message as sent by bridge.progressError 1140 @param error_msg(unicode): error message as sent by bridge.progressError
998 """ 1141 """
999 self.disp(_("Error while doing operation: {}").format(error_msg), error=True) 1142 self.disp(_(f"Error while doing operation: {e}"), error=True)
1000 1143
1001 def disp(self, msg, verbosity=0, error=False, no_lf=False): 1144 def disp(self, msg, verbosity=0, error=False, no_lf=False):
1002 return self.host.disp(msg, verbosity, error, no_lf) 1145 return self.host.disp(msg, verbosity, error, no_lf)
1003 1146
1004 def output(self, data): 1147 def output(self, data):
1005 try: 1148 try:
1006 output_type = self._output_type 1149 output_type = self._output_type
1007 except AttributeError: 1150 except AttributeError:
1008 raise exceptions.InternalError(_('trying to use output when use_output has not been set')) 1151 raise exceptions.InternalError(
1152 _('trying to use output when use_output has not been set'))
1009 return self.host.output(output_type, self.args.output, self.extra_outputs, data) 1153 return self.host.output(output_type, self.args.output, self.extra_outputs, data)
1010
1011 def exitCb(self, msg=None):
1012 """generic callback for success
1013
1014 optionally print a message, and quit
1015 msg(None, unicode): if not None, print this message
1016 """
1017 if msg is not None:
1018 self.disp(msg)
1019 self.host.quit(C.EXIT_OK)
1020
1021 def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR):
1022 """generic callback for errbacks
1023
1024 display failure_ then quit with generic error
1025 @param failure_: arguments returned by errback
1026 @param msg(unicode, None): message template
1027 use {} if you want to display failure message
1028 @param exit_code(int): shell exit code
1029 """
1030 if msg is None:
1031 msg = _("error: {}")
1032 self.disp(msg.format(failure_), error=True)
1033 self.host.quit(exit_code)
1034 1154
1035 def getPubsubExtra(self, extra=None): 1155 def getPubsubExtra(self, extra=None):
1036 """Helper method to compute extra data from pubsub arguments 1156 """Helper method to compute extra data from pubsub arguments
1037 1157
1038 @param extra(None, dict): base extra dict, or None to generate a new one 1158 @param extra(None, dict): base extra dict, or None to generate a new one
1088 1208
1089 # now we add subcommands to ourself 1209 # now we add subcommands to ourself
1090 for cls in subcommands: 1210 for cls in subcommands:
1091 cls(self) 1211 cls(self)
1092 1212
1093 def run(self): 1213 async def run(self):
1094 """this method is called when a command is actually run 1214 """this method is called when a command is actually run
1095 1215
1096 It set stuff like progression callbacks and profile connection 1216 It set stuff like progression callbacks and profile connection
1097 You should not overide this method: you should call self.start instead 1217 You should not overide this method: you should call self.start instead
1098 """ 1218 """
1099 # we keep a reference to run command, it may be useful e.g. for outputs 1219 # we keep a reference to run command, it may be useful e.g. for outputs
1100 self.host.command = self 1220 self.host.command = self
1101 # host._need_loop is set here from our current value and not before
1102 # as the need_loop decision must be taken only by then running command
1103 self.host._need_loop = self.need_loop
1104 1221
1105 try: 1222 try:
1106 show_progress = self.args.progress 1223 show_progress = self.args.progress
1107 except AttributeError: 1224 except AttributeError:
1108 # the command doesn't use progress bar 1225 # the command doesn't use progress bar
1109 pass 1226 pass
1110 else: 1227 else:
1111 if show_progress: 1228 if show_progress:
1112 self.host.watch_progress = True 1229 self.host.watch_progress = True
1113 # we need to register the following signal even if we don't display the progress bar 1230 # we need to register the following signal even if we don't display the
1114 self.host.bridge.register_signal("progressStarted", self.progressStartedHandler) 1231 # progress bar
1115 self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler) 1232 self.host.bridge.register_signal(
1116 self.host.bridge.register_signal("progressError", self.progressErrorHandler) 1233 "progressStarted", self.progressStartedHandler)
1234 self.host.bridge.register_signal(
1235 "progressFinished", self.progressFinishedHandler)
1236 self.host.bridge.register_signal(
1237 "progressError", self.progressErrorHandler)
1117 1238
1118 if self.need_connect is not None: 1239 if self.need_connect is not None:
1119 self.host.connect_profile(self.connected) 1240 await self.host.connect_profile()
1120 else: 1241 await self.start()
1121 self.start() 1242
1122 1243 async def start(self):
1123 def connected(self): 1244 """This is the starting point of the command, this method must be overriden
1124 """this method is called when profile is connected (or session is started)
1125
1126 this method is only called when use_profile is True
1127 most of time you should override self.start instead of this method, but if loop
1128 if not always needed depending on your arguments, you may override this method,
1129 but don't forget to call the parent one (i.e. this one) after self.need_loop is set
1130 """
1131 if not self.need_loop:
1132 self.host.stop_loop()
1133 self.start()
1134
1135 def start(self):
1136 """This is the starting point of the command, this method should be overriden
1137 1245
1138 at this point, profile are connected if needed 1246 at this point, profile are connected if needed
1139 """ 1247 """
1140 pass 1248 raise NotImplementedError
1141 1249
1142 1250
1143 class CommandAnswering(CommandBase): 1251 class CommandAnswering(CommandBase):
1144 """Specialised commands which answer to specific actions 1252 """Specialised commands which answer to specific actions
1145 1253
1150 # which will manage the answer. profile filtering is 1258 # which will manage the answer. profile filtering is
1151 # already managed when callback is called 1259 # already managed when callback is called
1152 1260
1153 def __init__(self, *args, **kwargs): 1261 def __init__(self, *args, **kwargs):
1154 super(CommandAnswering, self).__init__(*args, **kwargs) 1262 super(CommandAnswering, self).__init__(*args, **kwargs)
1155 self.need_loop = True 1263
1156 1264 async def onActionNew(self, action_data, action_id, security_limit, profile):
1157 def onActionNew(self, action_data, action_id, security_limit, profile):
1158 if profile != self.profile: 1265 if profile != self.profile:
1159 return 1266 return
1160 try: 1267 try:
1161 action_type = action_data['meta_type'] 1268 action_type = action_data['meta_type']
1162 except KeyError: 1269 except KeyError:
1170 try: 1277 try:
1171 callback = self.action_callbacks[action_type] 1278 callback = self.action_callbacks[action_type]
1172 except KeyError: 1279 except KeyError:
1173 pass 1280 pass
1174 else: 1281 else:
1175 callback(action_data, action_id, security_limit, profile) 1282 await callback(action_data, action_id, security_limit, profile)
1176 1283
1177 def onXMLUI(self, xml_ui): 1284 def onXMLUI(self, xml_ui):
1178 """Display a dialog received from the backend. 1285 """Display a dialog received from the backend.
1179 1286
1180 @param xml_ui (unicode): dialog XML representation 1287 @param xml_ui (unicode): dialog XML representation
1185 ui = ET.fromstring(xml_ui.encode('utf-8')) 1292 ui = ET.fromstring(xml_ui.encode('utf-8'))
1186 dialog = ui.find("dialog") 1293 dialog = ui.find("dialog")
1187 if dialog is not None: 1294 if dialog is not None:
1188 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") 1295 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error")
1189 1296
1190 def connected(self): 1297 async def start_answering(self):
1191 """Auto reply to confirmations requests""" 1298 """Auto reply to confirmation requests"""
1192 self.need_loop = True
1193 super(CommandAnswering, self).connected()
1194 self.host.bridge.register_signal("actionNew", self.onActionNew) 1299 self.host.bridge.register_signal("actionNew", self.onActionNew)
1195 actions = self.host.bridge.actionsGet(self.profile) 1300 actions = await self.host.bridge.actionsGet(self.profile)
1196 for action_data, action_id, security_limit in actions: 1301 for action_data, action_id, security_limit in actions:
1197 self.onActionNew(action_data, action_id, security_limit, self.profile) 1302 await self.onActionNew(action_data, action_id, security_limit, self.profile)