changeset 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 a1bc34f90fa5
children 72583524cfd3
files sat/core/constants.py sat/core/sat_main.py sat/core/xmpp.py sat/memory/crypto.py sat/memory/disco.py sat/memory/memory.py sat/memory/params.py sat/plugins/plugin_adhoc_dbus.py sat/plugins/plugin_exp_events.py sat/plugins/plugin_exp_pubsub_schema.py sat/plugins/plugin_import.py sat/plugins/plugin_merge_req_mercurial.py sat/plugins/plugin_misc_email_invitation.py sat/plugins/plugin_misc_merge_requests.py sat/plugins/plugin_misc_text_syntaxes.py sat/plugins/plugin_misc_xmllog.py sat/plugins/plugin_sec_otr.py sat/plugins/plugin_xep_0048.py sat/plugins/plugin_xep_0054.py sat/plugins/plugin_xep_0065.py sat/plugins/plugin_xep_0166.py sat/plugins/plugin_xep_0234.py sat/plugins/plugin_xep_0300.py sat/plugins/plugin_xep_0329.py sat/plugins/plugin_xep_0363.py sat/tools/common/async_process.py sat_frontends/jp/base.py sat_frontends/jp/cmd_account.py sat_frontends/jp/cmd_adhoc.py sat_frontends/jp/cmd_avatar.py sat_frontends/jp/cmd_blog.py sat_frontends/jp/cmd_bookmarks.py sat_frontends/jp/cmd_debug.py sat_frontends/jp/cmd_encryption.py sat_frontends/jp/cmd_event.py sat_frontends/jp/cmd_file.py sat_frontends/jp/cmd_forums.py sat_frontends/jp/cmd_identity.py sat_frontends/jp/cmd_info.py sat_frontends/jp/cmd_input.py sat_frontends/jp/cmd_invitation.py sat_frontends/jp/cmd_merge_request.py sat_frontends/jp/cmd_message.py sat_frontends/jp/cmd_param.py sat_frontends/jp/cmd_ping.py sat_frontends/jp/cmd_pipe.py sat_frontends/jp/cmd_profile.py sat_frontends/jp/cmd_pubsub.py sat_frontends/jp/cmd_roster.py sat_frontends/jp/cmd_shell.py sat_frontends/jp/cmd_ticket.py sat_frontends/jp/cmd_uri.py sat_frontends/jp/common.py sat_frontends/jp/constants.py sat_frontends/jp/jp sat_frontends/jp/output_xmlui.py sat_frontends/jp/xmlui_manager.py sat_frontends/tools/xmlui.py setup.py twisted/plugins/sat_plugin.py
diffstat 60 files changed, 3328 insertions(+), 3329 deletions(-) [+]
line wrap: on
line diff
--- a/sat/core/constants.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/core/constants.py	Wed Sep 25 08:56:41 2019 +0200
@@ -334,6 +334,7 @@
 
     ## Progress error special values ##
     PROGRESS_ERROR_DECLINED = "declined"  #  session has been declined by peer user
+    PROGRESS_ERROR_FAILED = "failed"  #  something went wrong with the session
 
     ## Files ##
     FILE_TYPE_DIRECTORY = "directory"
--- a/sat/core/sat_main.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/core/sat_main.py	Wed Sep 25 08:56:41 2019 +0200
@@ -260,7 +260,7 @@
                 )
                 self._unimport_plugin(plugin_path)
                 continue
-            except Exception as e:
+            except Exception:
                 import traceback
 
                 log.error(
--- a/sat/core/xmpp.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/core/xmpp.py	Wed Sep 25 08:56:41 2019 +0200
@@ -86,11 +86,11 @@
         self.actions = {}  # used to keep track of actions for retrieval (key = action_id)
         self.encryption = encryption.EncryptionHandler(self)
 
-    def __unicode__(self):
-        return "Client instance for profile {profile}".format(profile=self.profile)
+    def __str__(self):
+        return f"Client for profile {self.profile}"
 
-    def __str__(self):
-        return self.__unicode__.encode('utf-8')
+    def __repr__(self):
+        return f"{super().__repr__()} - profile: {self.profile!r}"
 
     ## initialisation ##
 
--- a/sat/memory/crypto.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/memory/crypto.py	Wed Sep 25 08:56:41 2019 +0200
@@ -45,7 +45,7 @@
         @param key (unicode): the encryption key
         @param text (unicode): the text to encrypt
         @param leave_empty (bool): if True, empty text will be returned "as is"
-        @return: Deferred: base-64 encoded str
+        @return (D(str)): base-64 encoded encrypted message
         """
         if leave_empty and text == "":
             return succeed(text)
@@ -59,6 +59,7 @@
         cipher = AES.new(key, AES.MODE_CFB, iv)
         d = deferToThread(cipher.encrypt, BlockCipher.pad(text.encode("utf-8")))
         d.addCallback(lambda ciphertext: b64encode(iv + ciphertext))
+        d.addCallback(lambda bytes_cypher: bytes_cypher.decode('utf-8'))
         return d
 
     @classmethod
@@ -137,7 +138,7 @@
         @return: Deferred: base-64 encoded str
         """
         if leave_empty and password == "":
-            return succeed(b"")
+            return succeed("")
         salt = (
             b64decode(salt)[: PasswordHasher.SALT_LEN]
             if salt
@@ -145,11 +146,12 @@
         )
         d = deferToThread(PBKDF2, password, salt)
         d.addCallback(lambda hashed: b64encode(salt + hashed))
+        d.addCallback(lambda hashed_bytes: hashed_bytes.decode('utf-8'))
         return d
 
     @classmethod
     def compare_hash(cls, hashed_attempt, hashed):
-        assert isinstance(hashed, bytes)
+        assert isinstance(hashed, str)
         return hashed_attempt == hashed
 
     @classmethod
@@ -160,7 +162,9 @@
         @param hashed (str): the hash of the password
         @return: Deferred: boolean
         """
+        assert isinstance(attempt, str)
+        assert isinstance(hashed, str)
         leave_empty = hashed == ""
         d = PasswordHasher.hash(attempt, hashed, leave_empty)
-        d.addCallback(cls.compare_hash, hashed=hashed.encode('utf-8'))
+        d.addCallback(cls.compare_hash, hashed=hashed)
         return d
--- a/sat/memory/disco.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/memory/disco.py	Wed Sep 25 08:56:41 2019 +0200
@@ -434,7 +434,7 @@
             extensions[form_type or ""] = fields
 
         defer.returnValue((
-            disco_infos.features,
+            [str(f) for f in disco_infos.features],
             [(cat, type_, name or "")
              for (cat, type_), name in list(disco_infos.identities.items())],
             extensions))
--- a/sat/memory/memory.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/memory/memory.py	Wed Sep 25 08:56:41 2019 +0200
@@ -560,9 +560,11 @@
 
         def initPersonalKey(__):
             # be sure to call this after checking that the profile doesn't exist yet
+
+            # generated once for all and saved in a PersistentDict
             personal_key = BlockCipher.getRandomKey(
                 base64=True
-            )  # generated once for all and saved in a PersistentDict
+            ).decode('utf-8')
             self.auth_sessions.newSession(
                 {C.MEMORY_CRYPTO_KEY: personal_key}, profile=name
             )  # will be encrypted by setParam
@@ -1260,8 +1262,8 @@
             parent = current["parent"]
             if not parent:
                 break
-            files_data = yield self.getFile(
-                self, client, peer_jid=None, file_id=parent, perms_to_check=None
+            files_data = yield self.getFiles(
+                client, peer_jid=None, file_id=parent, perms_to_check=None
             )
             try:
                 current = files_data[0]
--- a/sat/memory/params.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/memory/params.py	Wed Sep 25 08:56:41 2019 +0200
@@ -529,17 +529,18 @@
 
         return d.addCallback(gotPlainPassword)
 
-    def __type_to_string(self, result):
-        """ convert result to string, according to its type """
+    def _type_to_str(self, result):
+        """Convert result to string, according to its type """
         if isinstance(result, bool):
-            return "true" if result else "false"
-        elif isinstance(result, int):
+            return C.boolConst(result)
+        elif isinstance(result, (list, set, tuple)):
+            return ', '.join(self._type_to_str(r) for r in result)
+        else:
             return str(result)
-        return result
 
     def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
         """ Same as getParamA but for bridge: convert non string value to string """
-        return self.__type_to_string(
+        return self._type_to_str(
             self.getParamA(name, category, attr, profile_key=profile_key)
         )
 
@@ -599,15 +600,10 @@
             return self._getAttr(node[1], attr, value)
 
     def asyncGetStringParamA(
-        self,
-        name,
-        category,
-        attr="value",
-        security_limit=C.NO_SECURITY_LIMIT,
-        profile_key=C.PROF_KEY_NONE,
-    ):
+        self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT,
+        profile_key=C.PROF_KEY_NONE):
         d = self.asyncGetParamA(name, category, attr, security_limit, profile_key)
-        d.addCallback(self.__type_to_string)
+        d.addCallback(self._type_to_str)
         return d
 
     def asyncGetParamA(
@@ -957,14 +953,8 @@
                 categories.append(cat.getAttribute("name"))
         return categories
 
-    def setParam(
-        self,
-        name,
-        value,
-        category,
-        security_limit=C.NO_SECURITY_LIMIT,
-        profile_key=C.PROF_KEY_NONE,
-    ):
+    def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT,
+                 profile_key=C.PROF_KEY_NONE):
         """Set a parameter, return None if the parameter is not in param xml.
 
         Parameter of type 'password' that are not the SàT profile password are
@@ -994,13 +984,11 @@
             return defer.succeed(None)
 
         if not self.checkSecurityLimit(node[1], security_limit):
-            log.warning(
-                _(
-                    "Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!"
-                    % {"param": name, "cat": category}
-                )
-            )
-            return defer.succeed(None)
+            msg = _(
+                f"{profile!r} is trying to set parameter {name!r} in category "
+                f"{category!r} without authorization!!!")
+            log.warning(msg)
+            raise exceptions.PermissionError(msg)
 
         type_ = node[1].getAttribute("type")
         if type_ == "int":
@@ -1010,12 +998,10 @@
                 try:
                     int(value)
                 except ValueError:
-                    log.debug(
-                        _(
-                            "Trying to set parameter '%(param)s' in category '%(cat)s' with an non-integer value"
-                            % {"param": name, "cat": category}
-                        )
-                    )
+                    log.warning(_(
+                        f"Trying to set parameter {name!r} in category {category!r} with"
+                        f"an non-integer value"
+                    ))
                     return defer.succeed(None)
                 if node[1].hasAttribute("constraint"):
                     constraint = node[1].getAttribute("constraint")
--- a/sat/plugins/plugin_adhoc_dbus.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_adhoc_dbus.py	Wed Sep 25 08:56:41 2019 +0200
@@ -236,7 +236,7 @@
                 flags=flags,
             )
 
-        defer.returnValue((bus_name, methods))
+        defer.returnValue((str(bus_name), methods))
 
     def _addCommand(self, client, adhoc_name, bus_name, methods, allowed_jids=None,
                     allowed_groups=None, allowed_magics=None, forbidden_jids=None,
--- a/sat/plugins/plugin_exp_events.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_exp_events.py	Wed Sep 25 08:56:41 2019 +0200
@@ -381,7 +381,7 @@
 
         if register:
             yield self.register(
-                client, service, None, {}, node, event_id, event_elt, creator=True)
+                client, None, {}, service, node, event_id, item_elt, creator=True)
         defer.returnValue(node)
 
     def _eventModify(self, service, node, id_, timestamp_update, data_update,
@@ -447,24 +447,29 @@
                 events.append((timestamp, data))
         defer.returnValue(events)
 
-    def _eventInviteeGet(self, service, node, profile_key):
+    def _eventInviteeGet(self, service, node, invitee_jid_s, profile_key):
         service = jid.JID(service) if service else None
         node = node if node else NS_EVENT
         client = self.host.getClient(profile_key)
-        return self.eventInviteeGet(client, service, node)
+        invitee_jid = jid.JID(invitee_jid_s) if invitee_jid_s else None
+        return self.eventInviteeGet(client, service, node, invitee_jid)
 
     @defer.inlineCallbacks
-    def eventInviteeGet(self, client, service, node):
+    def eventInviteeGet(self, client, service, node, invitee_jid=None):
         """Retrieve attendance from event node
 
         @param service(unicode, None): PubSub service
-        @param node(unicode): PubSub node of the event
+        @param node(unicode): PubSub node of the event's invitees
+        @param invitee_jid(jid.JId, None): jid of the invitee to retrieve (None to
+          retrieve profile's invitation). The bare jid correspond to the PubSub item id.
         @return (dict): a dict with current attendance status,
             an empty dict is returned if nothing has been answered yed
         """
+        if invitee_jid is None:
+            invitee_jid = client.jid
         try:
             items, metadata = yield self._p.getItems(
-                client, service, node, item_ids=[client.jid.userhost()]
+                client, service, node, item_ids=[invitee_jid.userhost()]
             )
             event_elt = next(items[0].elements(NS_EVENT, "invitee"))
         except (exceptions.NotFound, IndexError):
--- a/sat/plugins/plugin_exp_pubsub_schema.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_exp_pubsub_schema.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,11 +18,11 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from collections import Iterable
-import copy
 import itertools
 from zope.interface import implementer
 from twisted.words.protocols.jabber import jid
 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from twisted.words.xish import domish
 from twisted.internet import defer
 from wokkel import disco, iwokkel
 from wokkel import data_form
@@ -174,7 +174,16 @@
                 )
         elif isinstance(schema, data_form.Form):
             if copy_form:
-                schema = copy.deepcopy(schema)
+                # XXX: we don't use deepcopy as it will do an infinite loop if a
+                #      domish.Element is present in the form fields (happens for
+                #      XEP-0315 data forms XML Element)
+                schema = data_form.Form(
+                    formType = schema.formType,
+                    title = schema.title,
+                    instructions = schema.instructions[:],
+                    formNamespace = schema.formNamespace,
+                    fields = schema.fieldList,
+                )
             defer.returnValue(schema)
 
         try:
@@ -394,6 +403,18 @@
                 if not values_list:
                     # if values don't map to allowed values, we use default ones
                     values_list = field.values
+            elif field.ext_type == 'xml':
+                # FIXME: XML elements are not handled correctly, we need to know if we
+                #        actual XML/XHTML, or text to escape
+                for idx, value in enumerate(values_list[:]):
+                    if not isinstance(value, domish.Element):
+                        if field.value and field.value.uri == C.NS_XHTML:
+                            div_elt = domish.Element((C.NS_XHTML, 'div'))
+                            div_elt.addContent(str(value))
+                            values_list[idx] = div_elt
+                    else:
+                        raise NotImplementedError
+
             field.values = values_list
 
         yield self._p.sendItem(
@@ -546,9 +567,7 @@
             form = data_form.findForm(item_elt, form_ns)
             if form is None:
                 log.warning(
-                    _(
-                        "Can't parse previous item, update ignored: data form not found"
-                    ).format(reason=e)
+                    _("Can't parse previous item, update ignored: data form not found")
                 )
             else:
                 for name, field in form.fields.items():
--- a/sat/plugins/plugin_import.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_import.py	Wed Sep 25 08:56:41 2019 +0200
@@ -64,7 +64,7 @@
         @param name(unicode): import handler name
         """
         assert name == name.lower().strip()
-        log.info(_("initializing {name} import handler").format(name=name))
+        log.info(_(f"initializing {name} import handler"))
         import_handler.name = name
         import_handler.register = partial(self.register, import_handler)
         import_handler.unregister = partial(self.unregister, import_handler)
@@ -139,16 +139,8 @@
         else:
             return importer.short_desc, importer.long_desc
 
-    def _doImport(
-        self,
-        import_handler,
-        name,
-        location,
-        options,
-        pubsub_service="",
-        pubsub_node="",
-        profile=C.PROF_KEY_NONE,
-    ):
+    def _doImport(self, import_handler, name, location, options, pubsub_service="",
+                  pubsub_node="", profile=C.PROF_KEY_NONE):
         client = self.host.getClient(profile)
         options = {key: str(value) for key, value in options.items()}
         for option in import_handler.BOOL_OPTIONS:
@@ -159,9 +151,11 @@
         for option in import_handler.JSON_OPTIONS:
             try:
                 options[option] = json.loads(options[option])
+            except KeyError:
+                pass
             except ValueError:
                 raise exceptions.DataError(
-                    _("invalid json option: {name}").format(name=option)
+                    _(f"invalid json option: {option}")
                 )
         pubsub_service = jid.JID(pubsub_service) if pubsub_service else None
         return self.doImport(
@@ -175,16 +169,8 @@
         )
 
     @defer.inlineCallbacks
-    def doImport(
-        self,
-        client,
-        import_handler,
-        name,
-        location,
-        options=None,
-        pubsub_service=None,
-        pubsub_node=None,
-    ):
+    def doImport(self, client, import_handler, name, location, options=None,
+                 pubsub_service=None, pubsub_node=None,):
         """Import data
 
         @param import_handler(object): instance of the import handler
@@ -193,7 +179,8 @@
             can be an url, a file path, or anything which make sense
             check importer description for more details
         @param options(dict, None): extra options.
-        @param pubsub_service(jid.JID, None): jid of the PubSub service where data must be imported
+        @param pubsub_service(jid.JID, None): jid of the PubSub service where data must be
+            imported.
             None to use profile's server
         @param pubsub_node(unicode, None): PubSub node to use
             None to use importer's default node
--- a/sat/plugins/plugin_merge_req_mercurial.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_merge_req_mercurial.py	Wed Sep 25 08:56:41 2019 +0200
@@ -53,6 +53,7 @@
 
         @param path(unicode): path to the repository
         @param command(unicode): hg command to run
+        @return D(bytes): stdout of the command
         """
         assert "path" not in kwargs
         kwargs["path"] = path
@@ -86,8 +87,10 @@
         return d
 
     def export(self, repository):
-        return MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()',
-                                     '--encoding=utf-8')
+        d = MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()',
+                                  '--encoding=utf-8')
+        d.addCallback(lambda data: data.decode('utf-8'))
+        return d
 
     def import_(self, repository, data, data_type, item_id, service, node, extra):
         parsed_data = self.parse(data)
@@ -97,7 +100,7 @@
         except Exception:
             parsed_name = ''
         name = 'mr_{item_id}_{parsed_name}'.format(item_id=CLEAN_RE.sub('', item_id),
-                                                    parsed_name=parsed_name)
+                                                   parsed_name=parsed_name)
         return MercurialProtocol.run(repository, 'qimport', '-g', '--name', name,
                                      '--encoding=utf-8', '-', stdin=data)
 
--- a/sat/plugins/plugin_misc_email_invitation.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_misc_email_invitation.py	Wed Sep 25 08:56:41 2019 +0200
@@ -254,7 +254,7 @@
                     try:
                         yield self.host.plugins['XEP-0077'].registerNewAccount(jid_,
                                                                                password)
-                    except error.StanzaError as e:
+                    except error.StanzaError:
                         idx += 1
                     else:
                         break
@@ -321,7 +321,7 @@
             format_args['url'] = invite_url
 
             yield sat_email.sendEmail(
-                self.host,
+                self.host.memory.config,
                 [email] + emails_extra,
                 (kwargs.pop('message_subject', None) or DEFAULT_SUBJECT).format(
                     **format_args),
@@ -401,7 +401,7 @@
             C.PROF_KEY_NONE: don't filter invitations
         @return list(unicode): invitations uids
         """
-        invitations = yield list(self.invitations.items())
+        invitations = yield self.invitations.items()
         if profile != C.PROF_KEY_NONE:
             invitations = {id_:data for id_, data in invitations.items()
                            if data.get('profile') == profile}
--- a/sat/plugins/plugin_misc_merge_requests.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_misc_merge_requests.py	Wed Sep 25 08:56:41 2019 +0200
@@ -106,8 +106,8 @@
         @param name(unicode): name of the handler
         @param handler(object): instance of the handler.
             It must have the following methods, which may all return a Deferred:
-                - check(repository): True if repository can be handled
-                - export(repository): return export data, i.e. the patches
+                - check(repository)->bool: True if repository can be handled
+                - export(repository)->str: return export data, i.e. the patches
                 - parse(export_data): parse report data and return a list of dict
                                       (1 per patch) with:
                     - title: title of the commit message (first line)
--- a/sat/plugins/plugin_misc_text_syntaxes.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_misc_text_syntaxes.py	Wed Sep 25 08:56:41 2019 +0200
@@ -348,9 +348,8 @@
                     element.text = ''
         return html.tostring(xhtml_elt, encoding=str, method="xml")
 
-    def convert(
-        self, text, syntax_from, syntax_to=_SYNTAX_XHTML, safe=True, profile=None
-    ):
+    def convert(self, text, syntax_from, syntax_to=_SYNTAX_XHTML, safe=True,
+                profile=None):
         """Convert a text between two syntaxes
 
         @param text: text to convert
--- a/sat/plugins/plugin_misc_xmllog.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_misc_xmllog.py	Wed Sep 25 08:56:41 2019 +0200
@@ -20,10 +20,10 @@
 from sat.core.i18n import _
 from sat.core.constants import Const as C
 from sat.core.log import getLogger
+from twisted.words.xish import domish
+from functools import partial
 
 log = getLogger(__name__)
-from twisted.words.xish import domish
-from functools import partial
 
 PLUGIN_INFO = {
     C.PI_NAME: "Raw XML log Plugin",
@@ -74,9 +74,9 @@
 
     def onSend(self, obj, client):
         if isinstance(obj, str):
-            log = str(obj)
+            xml_log = obj
         elif isinstance(obj, domish.Element):
-            log = obj.toXml()
+            xml_log = obj.toXml()
         else:
             log.error(_("INTERNAL ERROR: Unmanaged XML type"))
-        self.host.bridge.xmlLog("OUT", log, client.profile)
+        self.host.bridge.xmlLog("OUT", xml_log, client.profile)
--- a/sat/plugins/plugin_sec_otr.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_sec_otr.py	Wed Sep 25 08:56:41 2019 +0200
@@ -108,7 +108,7 @@
             message data when an encrypted message is going to be sent
         """
         assert isinstance(self.peer, jid.JID)
-        msg = msg_str
+        msg = msg_str.decode('utf-8')
         client = self.user.client
         log.debug("injecting encrypted message to {to}".format(to=self.peer))
         if appdata is None:
@@ -530,7 +530,7 @@
             )  # FIXME: temporary and unsecure, must be changed when frontends
                #        are refactored
         otrctx = client._otr_context_manager.getContextForUser(to_jid)
-        query = otrctx.sendMessage(0, "?OTRv?")
+        query = otrctx.sendMessage(0, b"?OTRv?")
         otrctx.inject(query)
 
     def _otrSessionEnd(self, menu_data, profile):
--- a/sat/plugins/plugin_xep_0048.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0048.py	Wed Sep 25 08:56:41 2019 +0200
@@ -241,12 +241,12 @@
             except KeyError:
                 pass
 
-        for url in urls_data:
+        for url, url_data in urls_data.items():
             url_elt = storage_elt.addElement("url")
             url_elt[XEP_0048.URL_KEY] = url
             for attr in XEP_0048.URL_ATTRS:
                 try:
-                    url_elt[attr] = url[attr]
+                    url_elt[attr] = url_data[attr]
                 except KeyError:
                     pass
 
@@ -440,7 +440,10 @@
                 return ret
             data = bookmarks_ori[type_]
             for bookmark in data:
-                ret[_storage_location][bookmark.full()] = data[bookmark].copy()
+                if type_ == XEP_0048.MUC_TYPE:
+                    ret[_storage_location][bookmark.full()] = data[bookmark].copy()
+                else:
+                    ret[_storage_location][bookmark] = data[bookmark].copy()
             return ret
 
         for _storage_location in ("local", "private", "pubsub"):
--- a/sat/plugins/plugin_xep_0054.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0054.py	Wed Sep 25 08:56:41 2019 +0200
@@ -44,7 +44,7 @@
     raise exceptions.MissingModule(
         "Missing module pillow, please download/install it from https://python-pillow.github.io"
     )
-from io import StringIO
+import io
 
 try:
     from twisted.words.protocols.xmlstream import XMPPHandler
@@ -52,7 +52,8 @@
     from wokkel.subprotocols import XMPPHandler
 
 AVATAR_PATH = "avatars"
-AVATAR_DIM = (64, 64)  #  FIXME: dim are not adapted to modern resolutions !
+# AVATAR_DIM = (64, 64)  #  FIXME: dim are not adapted to modern resolutions !
+AVATAR_DIM = (128, 128)
 
 IQ_GET = '/iq[@type="get"]'
 NS_VCARD = "vcard-temp"
@@ -318,7 +319,7 @@
                     avatar_hash = yield threads.deferToThread(
                         self.savePhoto, client, elem, entity_jid
                     )
-                except (exceptions.DataError, exceptions.NotFound) as e:
+                except (exceptions.DataError, exceptions.NotFound):
                     avatar_hash = ""
                     vcard_dict["avatar"] = avatar_hash
                 except Exception as e:
@@ -515,12 +516,13 @@
                     left += offset
                     right -= offset
                 img = img.crop((left, upper, right, lower))
-        img_buf = StringIO()
+        img_buf = io.BytesIO()
         img.save(img_buf, "PNG")
 
         photo_elt = vcard_elt.addElement("PHOTO")
         photo_elt.addElement("TYPE", content="image/png")
-        photo_elt.addElement("BINVAL", content=b64encode(img_buf.getvalue()))
+        image_b64 = b64encode(img_buf.getvalue()).decode('utf-8')
+        photo_elt.addElement("BINVAL", content=image_b64)
         image_hash = sha1(img_buf.getvalue()).hexdigest()
         with client.cache.cacheData(
             PLUGIN_INFO["import_name"], image_hash, "image/png", MAX_AGE
--- a/sat/plugins/plugin_xep_0065.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0065.py	Wed Sep 25 08:56:41 2019 +0200
@@ -175,17 +175,8 @@
 
 
 class Candidate(object):
-    def __init__(
-        self,
-        host,
-        port,
-        type_,
-        priority,
-        jid_,
-        id_=None,
-        priority_local=False,
-        factory=None,
-    ):
+    def __init__(self, host, port, type_, priority, jid_, id_=None, priority_local=False,
+                 factory=None,):
         """
         @param host(unicode): host IP or domain
         @param port(int): port
@@ -227,15 +218,6 @@
         return self._priority
 
     def __str__(self):
-        # similar to __unicode__ but we don't show jid and we encode id
-        return "Candidate ({0.priority}): host={0.host} port={0.port} type={0.type}{id}".format(
-            self,
-            id=" id={}".format(self.id if self.id is not None else "").encode(
-                "utf-8", "ignore"
-            ),
-        )
-
-    def __unicode__(self):
         return "Candidate ({0.priority}): host={0.host} port={0.port} jid={0.jid} type={0.type}{id}".format(
             self, id=" id={}".format(self.id if self.id is not None else "")
         )
--- a/sat/plugins/plugin_xep_0166.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0166.py	Wed Sep 25 08:56:41 2019 +0200
@@ -113,9 +113,11 @@
         try:
             del client.jingle_sessions[sid]
         except KeyError:
-            log.debug("Jingle session id [{}] is unknown, nothing to delete".format(sid))
+            log.debug(
+                f"Jingle session id {sid!r} is unknown, nothing to delete "
+                f"[{client.profile}]")
         else:
-            log.debug("Jingle session id [{}] deleted".format(sid))
+            log.debug(f"Jingle session id {sid!r} deleted [{client.profile}]")
 
     ## helpers methods to build stanzas ##
 
@@ -146,7 +148,7 @@
                     condition=error_condition
                 )
             )
-        client.send(iq_elt)
+        return client.send(iq_elt)
 
     def _terminateEb(self, failure_):
         log.warning(_("Error while terminating session: {msg}").format(msg=failure_))
@@ -188,23 +190,22 @@
         )
         self._delSession(client, sid)
 
-    def _jingleErrorCb(self, fail, sid, request, client):
+    def _jingleErrorCb(self, failure_, session, request, client):
         """Called when something is going wrong while parsing jingle request
 
         The error condition depend of the exceptions raised:
             exceptions.DataError raise a bad-request condition
         @param fail(failure.Failure): the exceptions raised
-        @param sid(unicode): jingle session id
+        @param session(dict): data of the session
         @param request(domsih.Element): jingle request
         @param client: %(doc_client)s
         """
-        log.warning("Error while processing jingle request")
-        if isinstance(fail, exceptions.DataError):
-            self.sendError(client, "bad-request", sid, request)
+        log.warning(f"Error while processing jingle request [{client.profile}]")
+        if isinstance(failure_, exceptions.DataError):
+            return self.sendError(client, "bad-request", session['id'], request)
         else:
-            log.error("Unmanaged jingle exception")
-            self._delSession(client, sid)
-            raise fail
+            log.error(f"Unmanaged jingle exception: {failure_}")
+            return self.terminate(client, self.REASON_FAILED_APPLICATION, session)
 
     ## methods used by other plugins ##
 
@@ -706,19 +707,10 @@
         """
         return elt
 
-    def _callPlugins(
-        self,
-        client,
-        action,
-        session,
-        app_method_name="jingleHandler",
-        transp_method_name="jingleHandler",
-        app_default_cb=None,
-        transp_default_cb=None,
-        delete=True,
-        elements=True,
-        force_element=None,
-    ):
+    def _callPlugins(self, client, action, session, app_method_name="jingleHandler",
+                     transp_method_name="jingleHandler", app_default_cb=None,
+                     transp_default_cb=None, delete=True, elements=True,
+                     force_element=None):
         """Call application and transport plugin methods for all contents
 
         @param action(unicode): jingle action name
@@ -819,7 +811,7 @@
 
         confirm_dlist = defer.gatherResults(confirm_defers)
         confirm_dlist.addCallback(self._confirmationCb, session, jingle_elt, client)
-        confirm_dlist.addErrback(self._jingleErrorCb, session["id"], request, client)
+        confirm_dlist.addErrback(self._jingleErrorCb, session, request, client)
 
     def _confirmationCb(self, confirm_results, session, jingle_elt, client):
         """Method called when confirmation from user has been received
@@ -952,7 +944,7 @@
         negociate_defers = []
         negociate_defers = self._callPlugins(client, XEP_0166.A_SESSION_ACCEPT, session)
 
-        negociate_dlist = defer.DeferredList(negociate_defers)
+        negociate_dlist = defer.gatherResults(negociate_defers)
 
         # after negociations we start the transfer
         negociate_dlist.addCallback(
--- a/sat/plugins/plugin_xep_0234.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0234.py	Wed Sep 25 08:56:41 2019 +0200
@@ -55,7 +55,7 @@
     C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""),
 }
 
-EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash"}
+EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"}
 Range = namedtuple("Range", ("offset", "length"))
 
 
@@ -106,7 +106,7 @@
 
     # generic methods
 
-    def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None,
+    def buildFileElement(self, name=None, file_hash=None, hash_algo=None, size=None,
         mime_type=None, desc=None, modified=None, transfer_range=None, path=None,
         namespace=None, file_elt=None, **kwargs):
         """Generate a <file> element with available metadata
@@ -177,14 +177,8 @@
             file_data.update(kwargs)
         return self.buildFileElement(**file_data)
 
-    def parseFileElement(
-        self,
-        file_elt,
-        file_data=None,
-        given=False,
-        parent_elt=None,
-        keep_empty_range=False,
-    ):
+    def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None,
+                         keep_empty_range=False,):
         """Parse a <file> element and file dictionary accordingly
 
         @param file_data(dict, None): dict where the data will be set
@@ -194,10 +188,12 @@
         @param given(bool): if True, prefix hash key with "given_"
         @param parent_elt(domish.Element, None): parent of the file element
             if set, file_elt must not be set
-        @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None)
-            empty range are useful to know if a peer_jid can handle range
+        @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset
+            and length are None).
+            Empty range is useful to know if a peer_jid can handle range
         @return (dict): file_data
-        @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements
+        @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new
+            elements
         @raise exceptions.NotFound: there is not <file> element in parent_elt
         @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
         """
@@ -230,7 +226,7 @@
             # we don't want to go to parent dir when joining to a path
             name = "--"
             file_data["name"] = name
-        elif name is not None and "/" in name or "\\" in name:
+        elif name is not None and ("/" in name or "\\" in name):
             file_data["name"] = regex.pathEscape(name)
 
         try:
@@ -335,15 +331,8 @@
         defer.returnValue(progress_id)
 
     def _fileJingleRequest(
-        self,
-        peer_jid,
-        filepath,
-        name="",
-        file_hash="",
-        hash_algo="",
-        extra=None,
-        profile=C.PROF_KEY_NONE,
-    ):
+            self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,
+            profile=C.PROF_KEY_NONE):
         client = self.host.getClient(profile)
         return self.fileJingleRequest(
             client,
@@ -357,15 +346,8 @@
 
     @defer.inlineCallbacks
     def fileJingleRequest(
-        self,
-        client,
-        peer_jid,
-        filepath,
-        name=None,
-        file_hash=None,
-        hash_algo=None,
-        extra=None,
-    ):
+            self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None,
+            extra=None):
         """Request a file using jingle file transfer
 
         @param peer_jid(jid.JID): destinee jid
@@ -568,6 +550,7 @@
         d.addCallback(gotConfirmation)
         return d
 
+    @defer.inlineCallbacks
     def jingleHandler(self, client, action, session, content_name, desc_elt):
         content_data = session["contents"][content_name]
         application_data = content_data["application_data"]
@@ -579,7 +562,7 @@
                 next(file_elt.elements(NS_JINGLE_FT, "range"))
             except StopIteration:
                 # initiator doesn't manage <range>, but we do so we advertise it
-                #  FIXME: to be checked
+                # FIXME: to be checked
                 log.debug("adding <range> element")
                 file_elt.addElement("range")
         elif action == self._j.A_SESSION_ACCEPT:
@@ -596,21 +579,32 @@
                     size = int(str(size_elt))
                 except (StopIteration, ValueError):
                     size = None
-                # XXX: hash security is not critical here, so we just take the higher mandatory one
+                # XXX: hash security is not critical here, so we just take the higher
+                #      mandatory one
                 hasher = file_data["hash_hasher"] = self._hash.getHasher()
-                content_data["stream_object"] = stream.FileStreamObject(
-                    self.host,
-                    client,
-                    file_path,
-                    mode="wb",
-                    uid=self.getProgressId(session, content_name),
-                    size=size,
-                    data_cb=lambda data: hasher.update(data),
-                )
+                progress_id = self.getProgressId(session, content_name)
+                try:
+                    content_data["stream_object"] = stream.FileStreamObject(
+                        self.host,
+                        client,
+                        file_path,
+                        mode="wb",
+                        uid=progress_id,
+                        size=size,
+                        data_cb=lambda data: hasher.update(data),
+                    )
+                except Exception as e:
+                    self.host.bridge.progressError(
+                        progress_id, C.PROGRESS_ERROR_FAILED, client.profile
+                    )
+                    yield self._j.terminate(
+                        client, self._j.REASON_FAILED_APPLICATION, session)
+                    raise e
             else:
                 # we are sending the file
                 size = file_data["size"]
-                # XXX: hash security is not critical here, so we just take the higher mandatory one
+                # XXX: hash security is not critical here, so we just take the higher
+                #      mandatory one
                 hasher = file_data["hash_hasher"] = self._hash.getHasher()
                 content_data["stream_object"] = stream.FileStreamObject(
                     self.host,
@@ -625,7 +619,7 @@
             finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
         else:
             log.warning("FIXME: unmanaged action {}".format(action))
-        return desc_elt
+        defer.returnValue(desc_elt)
 
     def jingleSessionInfo(self, client, action, session, content_name, jingle_elt):
         """Called on session-info action
@@ -679,6 +673,16 @@
             self.host.bridge.progressError(
                 progress_id, C.PROGRESS_ERROR_DECLINED, client.profile
             )
+        elif not jingle_elt.success:
+            progress_id = self.getProgressId(session, content_name)
+            first_child = jingle_elt.firstChildElement()
+            if first_child is not None:
+                reason = first_child.name
+            else:
+                reason = C.PROGRESS_ERROR_FAILED
+            self.host.bridge.progressError(
+                progress_id, reason, client.profile
+            )
 
     def _sendCheckSum(self, client, session, content_name, content_data):
         """Send the session-info with the hash checksum"""
@@ -721,10 +725,10 @@
                 return True
             return False
         hasher = file_data["hash_hasher"]
-        hash_ = hasher.hexdigest().encode('utf-8')
+        hash_ = hasher.hexdigest()
 
         if hash_ == given_hash:
-            log.info("Hash checked, file was successfully transfered: {}".format(hash_))
+            log.info(f"Hash checked, file was successfully transfered: {hash_}")
             progress_metadata = {
                 "hash": hash_,
                 "hash_algo": file_data["hash_algo"],
--- a/sat/plugins/plugin_xep_0300.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0300.py	Wed Sep 25 08:56:41 2019 +0200
@@ -196,13 +196,13 @@
             try:
                 idx = algos.index(algo)
             except ValueError:
-                log.warning("Proposed {} algorithm is not managed".format(algo))
+                log.warning(f"Proposed {algo} algorithm is not managed")
                 algo = None
                 continue
 
             if best_algo is None or algos.index(best_algo) < idx:
                 best_algo = algo
-                best_value = base64.b64decode(str(hash_elt))
+                best_value = base64.b64decode(str(hash_elt)).decode('utf-8')
 
         if not hash_elt:
             raise exceptions.NotFound
--- a/sat/plugins/plugin_xep_0329.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0329.py	Wed Sep 25 08:56:41 2019 +0200
@@ -595,10 +595,14 @@
         ):
             return
         for file_data in files_data:
-            file_elt = self._jf.buildFileElementFromDict(
-                file_data, modified=file_data.get("modified", file_data["created"])
-            )
-            query_elt.addChild(file_elt)
+            if file_data['type'] == C.FILE_TYPE_DIRECTORY:
+                directory_elt = query_elt.addElement("directory")
+                directory_elt['name'] = file_data['name']
+            else:
+                file_elt = self._jf.buildFileElementFromDict(
+                    file_data, modified=file_data.get("modified", file_data["created"])
+                )
+                query_elt.addChild(file_elt)
         client.send(iq_result_elt)
 
     def onComponentRequest(self, iq_elt, client):
@@ -624,7 +628,7 @@
                 file_data = {"name": elt["name"], "type": C.FILE_TYPE_DIRECTORY}
             else:
                 log.warning(
-                    _("unexpected element, ignoring: {elt}").format(elt=elt.toXml())
+                    _(f"unexpected element, ignoring: {elt.toXml()}")
                 )
                 continue
             files.append(file_data)
@@ -642,7 +646,7 @@
 
     def _listFiles(self, target_jid, path, extra, profile):
         client = self.host.getClient(profile)
-        target_jid = client.jid.userhostJID() if not target_jid else jid.JID(target_jid)
+        target_jid = client.jid if not target_jid else jid.JID(target_jid)
         d = self.listFiles(client, target_jid, path or None)
         d.addCallback(self._serializeData)
         return d
--- a/sat/plugins/plugin_xep_0363.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/plugins/plugin_xep_0363.py	Wed Sep 25 08:56:41 2019 +0200
@@ -36,7 +36,6 @@
 from twisted.web import iweb
 from twisted.python import failure
 from collections import namedtuple
-from zope.interface import implementer
 from OpenSSL import SSL
 import os.path
 import mimetypes
@@ -204,7 +203,7 @@
         @param ignore_tls_errors(bool): ignore TLS certificate is True
         @return (tuple
         """
-        log.debug("Got upload slot: {}".format(slot))
+        log.debug(f"Got upload slot: {slot}")
         sat_file = self.host.plugins["FILE"].File(
             self.host, client, path, size=size, auto_end_signals=False
         )
@@ -222,7 +221,7 @@
             headers[name] = value
 
         d = agent.request(
-            "PUT",
+            b"PUT",
             slot.put.encode("utf-8"),
             http_headers.Headers(headers),
             file_producer,
--- a/sat/tools/common/async_process.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat/tools/common/async_process.py	Wed Sep 25 08:56:41 2019 +0200
@@ -76,8 +76,7 @@
     def processEnded(self, reason):
         data = b''.join(self.data)
         if (reason.value.exitCode == 0):
-            log.debug(_('{name!r} command succeed').format(
-                name=self.command_name.decode('utf-8', 'ignore')))
+            log.debug(_(f'{self.command_name!r} command succeed'))
             # we don't use "replace" on purpose, we want an exception if decoding
             # is not working properly
             self._deferred.callback(data)
@@ -106,7 +105,7 @@
             - stdin(unicode, None): data to push to standard input
             - verbose(bool): if True stdout and stderr will be logged
             other keyword arguments will be used in reactor.spawnProcess
-        @return ((D)): stdout in case of success
+        @return ((D)bytes): stdout in case of success
         @raise RuntimeError: command returned a non zero status
             stdin and stdout will be given as arguments
 
@@ -115,8 +114,6 @@
         if stdin is not None:
             stdin = stdin.encode('utf-8')
         verbose = kwargs.pop('verbose', False)
-        if 'path' in kwargs:
-            kwargs['path'] = kwargs['path'].encode('utf-8')
         args = [a.encode('utf-8') for a in args]
         kwargs = {k:v.encode('utf-8') for k,v in list(kwargs.items())}
         d = defer.Deferred()
--- a/sat_frontends/jp/base.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/base.py	Wed Sep 25 08:56:41 2019 +0200
@@ -17,18 +17,21 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import asyncio
 from sat.core.i18n import _
 
 ### logging ###
 import logging as log
-log.basicConfig(level=log.DEBUG,
-                format='%(message)s')
+log.basicConfig(level=log.WARNING,
+                format='[%(name)s] %(message)s')
 ###
 
 import sys
-import locale
+import os
 import os.path
 import argparse
+import inspect
+from pathlib import Path
 from glob import iglob
 from importlib import import_module
 from sat_frontends.tools.jid import JID
@@ -41,13 +44,20 @@
 from sat_frontends.jp.constants import Const as C
 from sat_frontends.tools import misc
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
-import shlex
 from collections import OrderedDict
 
 ## bridge handling
 # we get bridge name from conf and initialise the right class accordingly
 main_config = config.parseMainConf()
 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus')
+USER_INTER_MSG = _("User interruption: good bye")
+
+
+class QuitException(BaseException):
+    """Quitting is requested
+
+    This is used to stop execution when host.quit() is called
+    """
 
 
 # TODO: move loops handling in a separated module
@@ -63,8 +73,9 @@
         def run(self):
             self.loop.run()
 
-        def quit(self):
+        def quit(self, exit_code):
             self.loop.quit()
+            sys.exit(exit_code)
 
         def call_later(self, delay, callback, *args):
             """call a callback repeatedly
@@ -78,27 +89,66 @@
             GLib.timeout_add(delay, callback, *args)
 
 else:
-    print("can't start jp: only D-Bus bridge is currently handled")
-    sys.exit(C.EXIT_ERROR)
-    # FIXME: twisted loop can be used when jp can handle fully async bridges
-    # from twisted.internet import reactor
+    import signal
+    from twisted.internet import asyncioreactor
+    asyncioreactor.install()
+    from twisted.internet import reactor, defer
+
+    class JPLoop(object):
 
-    # class JPLoop(object):
+        def __init__(self):
+            # exit code must be set when using quit, so if it's not set
+            # something got wrong and we must report it
+            self._exit_code = C.EXIT_INTERNAL_ERROR
 
-    #     def run(self):
-    #         reactor.run()
+        def run(self, jp, *args):
+            self.jp = jp
+            signal.signal(signal.SIGINT, self._on_sigint)
+            defer.ensureDeferred(self._start(jp, *args))
+            try:
+                reactor.run(installSignalHandlers=False)
+            except SystemExit as e:
+                self._exit_code = e.code
+            sys.exit(self._exit_code)
 
-    #     def quit(self):
-    #         reactor.stop()
+        async def _start(self, jp, *args):
+            fut = asyncio.ensure_future(jp.main(*args))
+            try:
+                await defer.Deferred.fromFuture(fut)
+            except BaseException:
+                import traceback
+                traceback.print_exc()
+                jp.quit(1)
+
+        def quit(self, exit_code):
+            self._exit_code = exit_code
+            reactor.stop()
 
-    #     def _timeout_cb(self, args, callback, delay):
-    #         ret = callback(*args)
-    #         if ret:
-    #             reactor.callLater(delay, self._timeout_cb, args, callback, delay)
+        def _timeout_cb(self, args, callback, delay):
+            try:
+                ret = callback(*args)
+            # FIXME: temporary hack to avoid traceback when using XMLUI
+            #        to be removed once create_task is not used anymore in
+            #        xmlui_manager (i.e. once sat_frontends.tools.xmlui fully supports
+            #        async syntax)
+            except QuitException:
+                return
+            if ret:
+                reactor.callLater(delay, self._timeout_cb, args, callback, delay)
 
-    #     def call_later(self, delay, callback, *args):
-    #         delay = float(delay) / 1000
-    #         reactor.callLater(delay, self._timeout_cb, args, callback, delay)
+        def call_later(self, delay, callback, *args):
+            delay = float(delay) / 1000
+            reactor.callLater(delay, self._timeout_cb, args, callback, delay)
+
+        def _on_sigint(self, sig_number, stack_frame):
+            """Called on keyboard interruption
+
+            Print user interruption message, set exit code and stop reactor
+            """
+            print("\r" + USER_INTER_MSG)
+            self._exit_code = C.EXIT_USER_CANCELLED
+            reactor.callFromThread(reactor.stop)
+
 
 if bridge_name == "embedded":
     from sat.core import sat_main
@@ -107,9 +157,10 @@
 try:
     import progressbar
 except ImportError:
-    msg = (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') +
-           _('Progress bar deactivated\n--\n'))
-    print(msg.encode('utf-8'), file=sys.stderr)
+    msg = (_('ProgressBar not available, please download it at '
+             'http://pypi.python.org/pypi/progressbar\n'
+             'Progress bar deactivated\n--\n'))
+    print(msg, file=sys.stderr)
     progressbar=None
 
 #consts
@@ -122,7 +173,7 @@
 This is free software, and you are welcome to redistribute it under certain conditions.
 """
 
-PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms
+PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s
 
 
 def date_decoder(arg):
@@ -141,33 +192,30 @@
     def __init__(self):
         """
 
-        @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting,
-            or if you want the user to stop by himself
+        @attribute quit_on_progress_end (bool): set to False if you manage yourself
+            exiting, or if you want the user to stop by himself
         @attribute progress_success(callable): method to call when progress just started
             by default display a message
-        @attribute progress_success(callable): method to call when progress is successfully finished
-            by default display a message
+        @attribute progress_success(callable): method to call when progress is
+            successfully finished by default display a message
         @attribute progress_failure(callable): method to call when progress failed
             by default display a message
         """
-        # FIXME: need_loop should be removed, everything must be async in bridge so
-        #        loop will always be needed
         bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
         if bridge_module is None:
             log.error("Can't import {} bridge".format(bridge_name))
             sys.exit(1)
 
-        self.bridge = bridge_module.Bridge()
-        self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
+        self.bridge = bridge_module.AIOBridge()
+        self._onQuitCallbacks = []
 
-    def _bridgeCb(self):
-        self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
-                                              description=DESCRIPTION)
+    def _bridgeConnected(self):
+        self.parser = argparse.ArgumentParser(
+            formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION)
         self._make_parents()
         self.add_parser_options()
-        self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name')
-        self._auto_loop = False # when loop is used for internal reasons
-        self._need_loop = False
+        self.subparsers = self.parser.add_subparsers(
+            title=_('Available commands'), dest='command', required=True)
 
         # progress attributes
         self._progress_id = None # TODO: manage several progress ids
@@ -181,27 +229,14 @@
 
         self.own_jid = None  # must be filled at runtime if needed
 
-    def _bridgeEb(self, failure):
-        if isinstance(failure, exceptions.BridgeExceptionNoService):
-            print((_("Can't connect to SàT backend, are you sure it's launched ?")))
-        elif isinstance(failure, exceptions.BridgeInitError):
-            print((_("Can't init bridge")))
-        else:
-            print((_("Error while initialising bridge: {}".format(failure))))
-        sys.exit(C.EXIT_BRIDGE_ERROR)
-
-    @property
-    def version(self):
-        return self.bridge.getVersion()
-
     @property
     def progress_id(self):
         return self._progress_id
 
-    @progress_id.setter
-    def progress_id(self, value):
-        self._progress_id = value
-        self.replayCache('progress_ids_cache')
+    async def set_progress_id(self, progress_id):
+        # because we use async, we need an explicit setter
+        self._progress_id = progress_id
+        await self.replayCache('progress_ids_cache')
 
     @property
     def watch_progress(self):
@@ -224,13 +259,13 @@
         except AttributeError:
             return 0
 
-    def replayCache(self, cache_attribute):
+    async def replayCache(self, cache_attribute):
         """Replay cached signals
 
         @param cache_attribute(str): name of the attribute containing the cache
             if the attribute doesn't exist, there is no cache and the call is ignored
-            else the cache must be a list of tuples containing the replay callback as first item,
-            then the arguments to use
+            else the cache must be a list of tuples containing the replay callback as
+            first item, then the arguments to use
         """
         try:
             cache = getattr(self, cache_attribute)
@@ -238,7 +273,7 @@
             pass
         else:
             for cache_data in cache:
-                cache_data[0](*cache_data[1:])
+                await cache_data[0](*cache_data[1:])
 
     def disp(self, msg, verbosity=0, error=False, no_lf=False):
         """Print a message to user
@@ -260,23 +295,22 @@
                 else:
                     print(msg)
 
-    def output(self, type_, name, extra_outputs, data):
+    async def output(self, type_, name, extra_outputs, data):
         if name in extra_outputs:
-            extra_outputs[name](data)
+            method = extra_outputs[name]
         else:
-            self._outputs[type_][name]['callback'](data)
+            method = self._outputs[type_][name]['callback']
+
+        ret = method(data)
+        if inspect.isawaitable(ret):
+            await ret
 
     def addOnQuitCallback(self, callback, *args, **kwargs):
         """Add a callback which will be called on quit command
 
         @param callback(callback): method to call
         """
-        try:
-            callbacks_list = self._onQuitCallbacks
-        except AttributeError:
-            callbacks_list = self._onQuitCallbacks = []
-        finally:
-            callbacks_list.append((callback, args, kwargs))
+        self._onQuitCallbacks.append((callback, args, kwargs))
 
     def getOutputChoices(self, output_type):
         """Return valid output filters for output_type
@@ -289,33 +323,52 @@
     def _make_parents(self):
         self.parents = {}
 
-        # we have a special case here as the start-session option is present only if connection is not needed,
-        # so we create two similar parents, one with the option, the other one without it
+        # we have a special case here as the start-session option is present only if
+        # connection is not needed, so we create two similar parents, one with the
+        # option, the other one without it
         for parent_name in ('profile', 'profile_session'):
             parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
-            parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
-            parent.add_argument("--pwd", action="store", default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary"))
+            parent.add_argument(
+                "-p", "--profile", action="store", type=str, default='@DEFAULT@',
+                help=_("Use PROFILE profile key (default: %(default)s)"))
+            parent.add_argument(
+                "--pwd", action="store", default='', metavar='PASSWORD',
+                help=_("Password used to connect profile, if necessary"))
 
-        profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session']
+        profile_parent, profile_session_parent = (self.parents['profile'],
+                                                  self.parents['profile_session'])
 
-        connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _("Connect the profile before doing anything else")
-        profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
+        connect_short, connect_long, connect_action, connect_help = (
+            "-c", "--connect", "store_true",
+            _("Connect the profile before doing anything else")
+        )
+        profile_parent.add_argument(
+            connect_short, connect_long, action=connect_action, help=connect_help)
 
         profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group()
-        profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
-        profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting"))
+        profile_session_connect_group.add_argument(
+            connect_short, connect_long, action=connect_action, help=connect_help)
+        profile_session_connect_group.add_argument(
+            "--start-session", action="store_true",
+            help=_("Start a profile session without connecting"))
 
-        progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False)
+        progress_parent = self.parents['progress'] = argparse.ArgumentParser(
+            add_help=False)
         if progressbar:
-            progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar"))
+            progress_parent.add_argument(
+                "-P", "--progress", action="store_true", help=_("Show progress bar"))
 
         verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
-        verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_("Add a verbosity level (can be used multiple times)"))
+        verbose_parent.add_argument(
+            '--verbose', '-v', action='count', default=0,
+            help=_("Add a verbosity level (can be used multiple times)"))
 
         draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
         draft_group = draft_parent.add_argument_group(_('draft handling'))
-        draft_group.add_argument("-D", "--current", action="store_true", help=_("load current draft"))
-        draft_group.add_argument("-F", "--draft-path", help=_("path to a draft file to retrieve"))
+        draft_group.add_argument(
+            "-D", "--current", action="store_true", help=_("load current draft"))
+        draft_group.add_argument(
+            "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve"))
 
 
     def make_pubsub_group(self, flags, defaults):
@@ -356,11 +409,14 @@
                     item_help += _(" (DEFAULT: {default})".format(default=default))
             pubsub_group.add_argument("-i", "--item", default='',
                                       help=item_help)
-            pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_('retrieve last item'))
+            pubsub_group.add_argument(
+                "-L", "--last-item", action='store_true', help=_('retrieve last item'))
         elif flags.multi_items:
             # mutiple items, this activate several features: max-items, RSM, MAM
             # and Orbder-by
-            pubsub_group.add_argument("-i", "--item", action='append', dest='items', default=[], help=_("items to retrieve (DEFAULT: all)"))
+            pubsub_group.add_argument(
+                "-i", "--item", action='append', dest='items', default=[],
+                help=_("items to retrieve (DEFAULT: all)"))
             if not flags.no_max:
                 max_group = pubsub_group.add_mutually_exclusive_group()
                 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args
@@ -409,14 +465,22 @@
                 help=_("how items should be ordered"))
 
         if not flags.all_used:
-            raise exceptions.InternalError('unknown flags: {flags}'.format(flags=', '.join(flags.unused)))
+            raise exceptions.InternalError('unknown flags: {flags}'.format(
+                flags=', '.join(flags.unused)))
         if defaults:
-            raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults))
+            raise exceptions.InternalError(f'unused defaults: {defaults}')
 
         return parent
 
     def add_parser_options(self):
-        self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
+        self.parser.add_argument(
+            '--version',
+            action='version',
+            version=("{name} {version} {copyleft}".format(
+                name = PROG_NAME,
+                version = self.version,
+                copyleft = COPYLEFT))
+        )
 
     def register_output(self, type_, name, callback, description="", default=False):
         if type_ not in C.OUTPUT_TYPES:
@@ -427,7 +491,9 @@
                                      }
         if default:
             if type_ in self.default_output:
-                self.disp(_('there is already a default output for {}, ignoring new one').format(type_))
+                self.disp(
+                    _(f'there is already a default output for {type_}, ignoring new one')
+                )
             else:
                 self.default_output[type_] = name
 
@@ -445,7 +511,8 @@
 
     def check_output_options(self, accepted_set, options):
         if not accepted_set.issuperset(options):
-            self.disp("The following output options are invalid: {invalid_options}".format(
+            self.disp(
+                _("The following output options are invalid: {invalid_options}").format(
                 invalid_options = ', '.join(set(options).difference(accepted_set))),
                 error=True)
             self.quit(C.EXIT_BAD_ARG)
@@ -457,17 +524,20 @@
         """
         path = os.path.dirname(sat_frontends.jp.__file__)
         # XXX: outputs must be imported before commands as they are used for arguments
-        for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')):
-            modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern))))
+        for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'),
+                               (C.PLUGIN_CMD, 'cmd_*.py')):
+            modules = (
+                os.path.splitext(module)[0]
+                for module in map(os.path.basename, iglob(os.path.join(path, pattern))))
             for module_name in modules:
                 module_path = "sat_frontends.jp." + module_name
                 try:
                     module = import_module(module_path)
                     self.import_plugin_module(module, type_)
                 except ImportError as e:
-                    self.disp(_("Can't import {module_path} plugin, ignoring it: {msg}".format(
-                    module_path = module_path,
-                    msg = e)), error=True)
+                    self.disp(
+                        _(f"Can't import {module_path} plugin, ignoring it: {e}"),
+                        error=True)
                 except exceptions.CancelError:
                     continue
                 except exceptions.MissingModule as e:
@@ -485,7 +555,7 @@
         try:
             class_names =  getattr(module, '__{}__'.format(type_))
         except AttributeError:
-            log.disp(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True)
+            log.disp(_(f"Invalid plugin module [{type_}] {module}"), error=True)
             raise ImportError
         else:
             for class_name in class_names:
@@ -500,12 +570,15 @@
             scheme = 'http'
         else:
             raise exceptions.InternalError('An HTTP scheme is expected in this method')
-        self.disp("{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1)
+        self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1)
         # HTTP URL, we try to find xmpp: links
         try:
             from lxml import etree
         except ImportError:
-            self.disp("lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
+            self.disp(
+                "lxml module must be installed to use http(s) scheme, please install it "
+                "with \"pip install lxml\"",
+                error=True)
             self.quit(1)
         import urllib.request, urllib.error, urllib.parse
         parser = etree.HTMLParser()
@@ -517,7 +590,10 @@
         else:
             links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
         if not links:
-            self.disp('Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
+            self.disp(
+                _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP '
+                  'PubSub node/item'),
+                error=True)
             self.quit(1)
         xmpp_uri = links[0].get('href')
         return xmpp_uri
@@ -552,7 +628,10 @@
                             try:
                                 items = self.args.items
                             except AttributeError:
-                                self.disp(_("item specified in URL but not needed in command, ignoring it"), error=True)
+                                self.disp(
+                                    _("item specified in URL but not needed in command, "
+                                      "ignoring it"),
+                                    error=True)
                             else:
                                 if not items:
                                     self.args.items = [uri_item]
@@ -565,7 +644,7 @@
                                 if not item_last:
                                     self.args.item = uri_item
                 else:
-                    self.parser.error(_('XMPP URL is not a pubsub one: {url}').format(url=url))
+                    self.parser.error(_(f'XMPP URL is not a pubsub one: {url}'))
         flags = self.args._cmd._pubsub_flags
         # we check required arguments here instead of using add_arguments' required option
         # because the required argument can be set in URL
@@ -580,7 +659,8 @@
         #        so we check conflict here. This may be fixed in Python 3, to be checked
         try:
             if self.args.item and self.args.item_last:
-                self.parser.error(_("--item and --item-last can't be used at the same time"))
+                self.parser.error(
+                    _("--item and --item-last can't be used at the same time"))
         except AttributeError:
             pass
 
@@ -605,46 +685,78 @@
             if self.args.max is None:
                 self.args.max = C.NO_LIMIT
 
-    def run(self, args=None, namespace=None):
-        self.args = self.parser.parse_args(args, namespace=None)
-        if self.args._cmd._use_pubsub:
-            self.parse_pubsub_args()
+    async def main(self, args, namespace):
         try:
-            self.args._cmd.run()
-            if self._need_loop or self._auto_loop:
-                self._start_loop()
-        except KeyboardInterrupt:
-            self.disp(_("User interruption: good bye"))
+            await self.bridge.bridgeConnect()
+        except Exception as e:
+            if isinstance(e, exceptions.BridgeExceptionNoService):
+                print((_("Can't connect to SàT backend, are you sure it's launched ?")))
+            elif isinstance(e, exceptions.BridgeInitError):
+                print((_("Can't init bridge")))
+            else:
+                print((_(f"Error while initialising bridge: {e}")))
+            self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
+            return
+        self.version = await self.bridge.getVersion()
+        self._bridgeConnected()
+        self.import_plugins()
+        try:
+            self.args = self.parser.parse_args(args, namespace=None)
+            if self.args._cmd._use_pubsub:
+                self.parse_pubsub_args()
+            await self.args._cmd.run()
+        except SystemExit as e:
+            self.quit(e.code, raise_exc=False)
+            return
+        except QuitException:
+            return
 
-    def _start_loop(self):
+    def run(self, args=None, namespace=None):
         self.loop = JPLoop()
-        self.loop.run()
+        self.loop.run(self, args, namespace)
 
-    def stop_loop(self):
-        try:
-            self.loop.quit()
-        except AttributeError:
-            pass
+    def _read_stdin(self, stdin_fut):
+        """Callback called by ainput to read stdin"""
+        line = sys.stdin.readline()
+        if line:
+            stdin_fut.set_result(line.rstrip(os.linesep))
+        else:
+            stdin_fut.set_exception(EOFError())
 
-    def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")):
+    async def ainput(self, msg=''):
+        """Asynchronous version of buildin "input" function"""
+        self.disp(msg, no_lf=True)
+        sys.stdout.flush()
+        loop = asyncio.get_running_loop()
+        stdin_fut = loop.create_future()
+        loop.add_reader(sys.stdin, self._read_stdin, stdin_fut)
+        return await stdin_fut
+
+    async def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")):
         """Request user to confirm action, and quit if he doesn't"""
 
-        res = input("{} (y/N)? ".format(message))
+        res = await self.ainput(f"{message} (y/N)? ")
         if res not in ("y", "Y"):
             self.disp(cancel_message)
             self.quit(C.EXIT_USER_CANCELLED)
 
-    def quitFromSignal(self, errcode=0):
-        """Same as self.quit, but from a signal handler
+    def quitFromSignal(self, exit_code=0):
+        r"""Same as self.quit, but from a signal handler
 
         /!\: return must be used after calling this method !
         """
-        assert self._need_loop
         # XXX: python-dbus will show a traceback if we exit in a signal handler
         # so we use this little timeout trick to avoid it
-        self.loop.call_later(0, self.quit, errcode)
+        self.loop.call_later(0, self.quit, exit_code)
+
+    def quit(self, exit_code=0, raise_exc=True):
+        """Terminate the execution with specified exit_code
 
-    def quit(self, errcode=0):
+        This will stop the loop.
+        @param exit_code(int): code to return when quitting the program
+        @param raise_exp(boolean): if True raise a QuitException to stop code execution
+            The default value should be used most of time.
+        """
         # first the onQuitCallbacks
         try:
             callbacks_list = self._onQuitCallbacks
@@ -654,10 +766,11 @@
             for callback, args, kwargs in callbacks_list:
                 callback(*args, **kwargs)
 
-        self.stop_loop()
-        sys.exit(errcode)
+        self.loop.quit(exit_code)
+        if raise_exc:
+            raise QuitException
 
-    def check_jids(self, jids):
+    async def check_jids(self, jids):
         """Check jids validity, transform roster name to corresponding jids
 
         @param profile: profile name
@@ -668,7 +781,7 @@
         names2jid = {}
         nodes2jid = {}
 
-        for contact in self.bridge.getContacts(self.profile):
+        for contact in await self.bridge.getContacts(self.profile):
             jid_s, attr, groups = contact
             _jid = JID(jid_s)
             try:
@@ -704,29 +817,20 @@
 
         return dest_jids
 
-    def connect_profile(self, callback):
-        """ Check if the profile is connected and do it if requested
+    async def connect_profile(self):
+        """Check if the profile is connected and do it if requested
 
-        @param callback: method to call when profile is connected
         @exit: - 1 when profile is not connected and --connect is not set
                - 1 when the profile doesn't exists
                - 1 when there is a connection error
         """
         # FIXME: need better exit codes
 
-        def cant_connect(failure):
-            log.error(_("Can't connect profile: {reason}").format(reason=failure))
-            self.quit(1)
-
-        def cant_start_session(failure):
-            log.error(_("Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure))
-            self.quit(1)
-
-        self.profile = self.bridge.profileNameGet(self.args.profile)
+        self.profile = await self.bridge.profileNameGet(self.args.profile)
 
         if not self.profile:
-            log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile))
-            self.quit(1)
+            log.error(_(f"The profile [{self.args.profile}] doesn't exist"))
+            self.quit(C.EXIT_ERROR)
 
         try:
             start_session = self.args.start_session
@@ -734,40 +838,49 @@
             pass
         else:
             if start_session:
-                self.bridge.profileStartSession(self.args.pwd, self.profile, lambda __: callback(), cant_start_session)
-                self._auto_loop = True
+                try:
+                    await self.bridge.profileStartSession(self.args.pwd, self.profile)
+                except Exception as e:
+                    self.disp(_(f"Can't start {self.profile}'s session: {e}"), err=True)
+                    self.quit(1)
                 return
-            elif not self.bridge.profileIsSessionStarted(self.profile):
+            elif not await self.bridge.profileIsSessionStarted(self.profile):
                 if not self.args.connect:
-                    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))
+                    self.disp(_(
+                        f"Session for [{self.profile}] is not started, please start it "
+                        f"before using jp, or use either --start-session or --connect "
+                        f"option"), error=True)
                     self.quit(1)
             elif not getattr(self.args, "connect", False):
-                callback()
                 return
 
 
         if not hasattr(self.args, 'connect'):
-            # a profile can be present without connect option (e.g. on profile creation/deletion)
+            # a profile can be present without connect option (e.g. on profile
+            # creation/deletion)
             return
         elif self.args.connect is True:  # if connection is asked, we connect the profile
-            self.bridge.connect(self.profile, self.args.pwd, {}, lambda __: callback(), cant_connect)
-            self._auto_loop = True
+            try:
+                await self.bridge.connect(self.profile, self.args.pwd, {})
+            except Exception as e:
+                self.disp(_(f"Can't connect profile: {e}"), error=True)
+                self.quit(1)
             return
         else:
-            if not self.bridge.isConnected(self.profile):
-                log.error(_("Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile))
+            if not await self.bridge.isConnected(self.profile):
+                log.error(
+                    _(f"Profile [{self.profile}] is not connected, please connect it "
+                      f"before using jp, or use --connect option"))
                 self.quit(1)
 
-        callback()
-
-    def get_full_jid(self, param_jid):
+    async def get_full_jid(self, param_jid):
         """Return the full jid if possible (add main resource when find a bare jid)"""
         _jid = JID(param_jid)
         if not _jid.resource:
             #if the resource is not given, we try to add the main resource
-            main_resource = self.bridge.getMainResource(param_jid, self.profile)
+            main_resource = await self.bridge.getMainResource(param_jid, self.profile)
             if main_resource:
-                return "%s/%s" % (_jid.bare, main_resource)
+                return f"{_jid.bare}/{main_resource}"
         return param_jid
 
 
@@ -783,7 +896,8 @@
         @param use_output(bool, unicode): if not False, add --output option
         @param extra_outputs(dict): list of command specific outputs:
             key is output name ("default" to use as main output)
-            value is a callable which will format the output (data will be used as only argument)
+            value is a callable which will format the output (data will be used as only
+            argument)
             if a key already exists with normal outputs, the extra one will be used
         @param need_connect(bool, None): True if profile connection is needed
             False else (profile session must still be started)
@@ -799,14 +913,13 @@
                 mandatory arguments are controlled by pubsub_req
             - use_draft(bool): if True, add draft handling options
             ** other arguments **
-            - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be:
+            - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options,
+              can be:
                 C.SERVICE: service is required
                 C.NODE: node is required
                 C.ITEM: item is required
                 C.SINGLE_ITEM: only one item is allowed
-        @attribute need_loop(bool): to set by commands when loop is needed
         """
-        self.need_loop = False # to be set by commands when loop is needed
         try: # If we have subcommands, host is a CommandBase and we need to use host.host
             self.host = host.host
         except AttributeError:
@@ -815,10 +928,12 @@
         # --profile option
         parents = kwargs.setdefault('parents', set())
         if use_profile:
-            #self.host.parents['profile'] is an ArgumentParser with profile connection arguments
+            # self.host.parents['profile'] is an ArgumentParser with profile connection
+            # arguments
             if need_connect is None:
                 need_connect = True
-            parents.add(self.host.parents['profile' if need_connect else 'profile_session'])
+            parents.add(
+                self.host.parents['profile' if need_connect else 'profile_session'])
         else:
             assert need_connect is None
         self.need_connect = need_connect
@@ -838,7 +953,8 @@
             choices = set(self.host.getOutputChoices(use_output))
             choices.update(extra_outputs)
             if not choices:
-                raise exceptions.InternalError("No choice found for {} output type".format(use_output))
+                raise exceptions.InternalError(
+                    "No choice found for {} output type".format(use_output))
             try:
                 default = self.host.default_output[use_output]
             except KeyError:
@@ -848,8 +964,12 @@
                     default = 'simple'
                 else:
                     default = list(choices)[0]
-            output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_("select output format (default: {})".format(default)))
-            output_parent.add_argument('--output-option', '--oo', action="append", dest='output_opts', default=[], help=_("output specific option"))
+            output_parent.add_argument(
+                '--output', '-O', choices=sorted(choices), default=default,
+                help=_("select output format (default: {})".format(default)))
+            output_parent.add_argument(
+                '--output-option', '--oo', action="append", dest='output_opts',
+                default=[], help=_("output specific option"))
             parents.add(output_parent)
         else:
             assert extra_outputs is None
@@ -873,7 +993,7 @@
 
         self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
         if hasattr(self, "subcommands"):
-            self.subparsers = self.parser.add_subparsers()
+            self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True)
         else:
             self.parser.set_defaults(_cmd=self)
         self.add_parser_options()
@@ -894,11 +1014,10 @@
     def progress_id(self):
         return self.host.progress_id
 
-    @progress_id.setter
-    def progress_id(self, value):
-        self.host.progress_id = value
+    async def set_progress_id(self, progress_id):
+        return await self.host.set_progress_id(progress_id)
 
-    def progressStartedHandler(self, uid, metadata, profile):
+    async def progressStartedHandler(self, uid, metadata, profile):
         if profile != self.profile:
             return
         if self.progress_id is None:
@@ -907,15 +1026,20 @@
             # when the progress_id is received
             cache_data = (self.progressStartedHandler, uid, metadata, profile)
             try:
-                self.host.progress_ids_cache.append(cache_data)
+                cache = self.host.progress_ids_cache
             except AttributeError:
-                self.host.progress_ids_cache = [cache_data]
+                cache = self.host.progress_ids_cache = []
+            cache.append(cache_data)
         else:
             if self.host.watch_progress and uid == self.progress_id:
-                self.onProgressStarted(metadata)
-                self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate)
+                await self.onProgressStarted(metadata)
+                while True:
+                    await asyncio.sleep(PROGRESS_DELAY)
+                    cont = await self.progressUpdate()
+                    if not cont:
+                        break
 
-    def progressFinishedHandler(self, uid, metadata, profile):
+    async def progressFinishedHandler(self, uid, metadata, profile):
         if profile != self.profile:
             return
         if uid == self.progress_id:
@@ -923,39 +1047,58 @@
                 self.host.pbar.finish()
             except AttributeError:
                 pass
-            self.onProgressFinished(metadata)
+            await self.onProgressFinished(metadata)
             if self.host.quit_on_progress_end:
                 self.host.quitFromSignal()
 
-    def progressErrorHandler(self, uid, message, profile):
+    async def progressErrorHandler(self, uid, message, profile):
         if profile != self.profile:
             return
         if uid == self.progress_id:
             if self.args.progress:
                 self.disp('') # progress is not finished, so we skip a line
             if self.host.quit_on_progress_end:
-                self.onProgressError(message)
-                self.host.quitFromSignal(1)
+                await self.onProgressError(message)
+                self.host.quitFromSignal(C.EXIT_ERROR)
 
-    def progressUpdate(self):
-        """This method is continualy called to update the progress bar"""
-        data = self.host.bridge.progressGet(self.progress_id, self.profile)
+    async def progressUpdate(self):
+        """This method is continualy called to update the progress bar
+
+        @return (bool): False to stop being called
+        """
+        data = await self.host.bridge.progressGet(self.progress_id, self.profile)
         if data:
             try:
                 size = data['size']
             except KeyError:
-                self.disp(_("file size is not known, we can't show a progress bar"), 1, error=True)
+                self.disp(_("file size is not known, we can't show a progress bar"), 1,
+                          error=True)
                 return False
             if self.host.pbar is None:
                 #first answer, we must construct the bar
-                self.host.pbar = progressbar.ProgressBar(max_value=int(size),
-                                                         widgets=[_("Progress: "),progressbar.Percentage(),
-                                                         " ",
-                                                         progressbar.Bar(),
-                                                         " ",
-                                                         progressbar.FileTransferSpeed(),
-                                                         " ",
-                                                         progressbar.ETA()])
+
+                # if the instance has a pbar_template attribute, it is used has model,
+                # else default one is used
+                # template is a list of part, where part can be either a str to show directly
+                # or a list where first argument is a name of a progressbar widget, and others
+                # are used as widget arguments
+                try:
+                    template = self.pbar_template
+                except AttributeError:
+                    template = [
+                        _("Progress: "), ["Percentage"], " ", ["Bar"], " ",
+                        ["FileTransferSpeed"], " ", ["ETA"]
+                    ]
+
+                widgets = []
+                for part in template:
+                    if isinstance(part, str):
+                        widgets.append(part)
+                    else:
+                        widget = getattr(progressbar, part.pop(0))
+                        widgets.append(widget(*part))
+
+                self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets)
                 self.host.pbar.start()
 
             self.host.pbar.update(int(data['position']))
@@ -963,11 +1106,11 @@
         elif self.host.pbar is not None:
             return False
 
-        self.onProgressUpdate(data)
+        await self.onProgressUpdate(data)
 
         return True
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         """Called when progress has just started
 
         can be overidden by a command
@@ -975,7 +1118,7 @@
         """
         self.disp(_("Operation started"), 2)
 
-    def onProgressUpdate(self, metadata):
+    async def onProgressUpdate(self, metadata):
         """Method called on each progress updata
 
         can be overidden by a command to handle progress metadata
@@ -983,7 +1126,7 @@
         """
         pass
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         """Called when progress has just finished
 
         can be overidden by a command
@@ -991,12 +1134,12 @@
         """
         self.disp(_("Operation successfully finished"), 2)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, e):
         """Called when a progress failed
 
         @param error_msg(unicode): error message as sent by bridge.progressError
         """
-        self.disp(_("Error while doing operation: {}").format(error_msg), error=True)
+        self.disp(_(f"Error while doing operation: {e}"), error=True)
 
     def disp(self, msg, verbosity=0, error=False, no_lf=False):
         return self.host.disp(msg, verbosity, error, no_lf)
@@ -1005,33 +1148,10 @@
         try:
             output_type = self._output_type
         except AttributeError:
-            raise exceptions.InternalError(_('trying to use output when use_output has not been set'))
+            raise exceptions.InternalError(
+                _('trying to use output when use_output has not been set'))
         return self.host.output(output_type, self.args.output, self.extra_outputs, data)
 
-    def exitCb(self, msg=None):
-        """generic callback for success
-
-        optionally print a message, and quit
-        msg(None, unicode): if not None, print this message
-        """
-        if msg is not None:
-            self.disp(msg)
-        self.host.quit(C.EXIT_OK)
-
-    def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR):
-        """generic callback for errbacks
-
-        display failure_ then quit with generic error
-        @param failure_: arguments returned by errback
-        @param msg(unicode, None): message template
-            use {} if you want to display failure message
-        @param exit_code(int): shell exit code
-        """
-        if msg is None:
-            msg = _("error: {}")
-        self.disp(msg.format(failure_), error=True)
-        self.host.quit(exit_code)
-
     def getPubsubExtra(self, extra=None):
         """Helper method to compute extra data from pubsub arguments
 
@@ -1090,7 +1210,7 @@
         for cls in subcommands:
             cls(self)
 
-    def run(self):
+    async def run(self):
         """this method is called when a command is actually run
 
         It set stuff like progression callbacks and profile connection
@@ -1098,9 +1218,6 @@
         """
         # we keep a reference to run command, it may be useful e.g. for outputs
         self.host.command = self
-        # host._need_loop is set here from our current value and not before
-        # as the need_loop decision must be taken only by then running command
-        self.host._need_loop = self.need_loop
 
         try:
             show_progress = self.args.progress
@@ -1110,34 +1227,25 @@
         else:
             if show_progress:
                 self.host.watch_progress = True
-            # we need to register the following signal even if we don't display the progress bar
-            self.host.bridge.register_signal("progressStarted", self.progressStartedHandler)
-            self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler)
-            self.host.bridge.register_signal("progressError", self.progressErrorHandler)
+            # we need to register the following signal even if we don't display the
+            # progress bar
+            self.host.bridge.register_signal(
+                "progressStarted", self.progressStartedHandler)
+            self.host.bridge.register_signal(
+                "progressFinished", self.progressFinishedHandler)
+            self.host.bridge.register_signal(
+                "progressError", self.progressErrorHandler)
 
         if self.need_connect is not None:
-             self.host.connect_profile(self.connected)
-        else:
-            self.start()
-
-    def connected(self):
-        """this method is called when profile is connected (or session is started)
+            await self.host.connect_profile()
+        await self.start()
 
-        this method is only called when use_profile is True
-        most of time you should override self.start instead of this method, but if loop
-        if not always needed depending on your arguments, you may override this method,
-        but don't forget to call the parent one (i.e. this one) after self.need_loop is set
-        """
-        if not self.need_loop:
-            self.host.stop_loop()
-        self.start()
-
-    def start(self):
-        """This is the starting point of the command, this method should be overriden
+    async def start(self):
+        """This is the starting point of the command, this method must be overriden
 
         at this point, profile are connected if needed
         """
-        pass
+        raise NotImplementedError
 
 
 class CommandAnswering(CommandBase):
@@ -1152,9 +1260,8 @@
 
     def __init__(self, *args, **kwargs):
         super(CommandAnswering, self).__init__(*args, **kwargs)
-        self.need_loop = True
 
-    def onActionNew(self, action_data, action_id, security_limit, profile):
+    async def onActionNew(self, action_data, action_id, security_limit, profile):
         if profile != self.profile:
             return
         try:
@@ -1172,7 +1279,7 @@
             except KeyError:
                 pass
             else:
-                callback(action_data, action_id, security_limit, profile)
+                await callback(action_data, action_id, security_limit, profile)
 
     def onXMLUI(self, xml_ui):
         """Display a dialog received from the backend.
@@ -1187,11 +1294,9 @@
         if dialog is not None:
             self.disp(dialog.findtext("message"), error=dialog.get("level") == "error")
 
-    def connected(self):
-        """Auto reply to confirmations requests"""
-        self.need_loop = True
-        super(CommandAnswering, self).connected()
+    async def start_answering(self):
+        """Auto reply to confirmation requests"""
         self.host.bridge.register_signal("actionNew", self.onActionNew)
-        actions = self.host.bridge.actionsGet(self.profile)
+        actions = await self.host.bridge.actionsGet(self.profile)
         for action_data, action_id, security_limit in actions:
-            self.onActionNew(action_data, action_id, security_limit, self.profile)
+            await self.onActionNew(action_data, action_id, security_limit, self.profile)
--- a/sat_frontends/jp/cmd_account.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_account.py	Wed Sep 25 08:56:41 2019 +0200
@@ -39,7 +39,6 @@
             use_verbose=True,
             help=_("create a XMPP account"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -72,80 +71,81 @@
             "--port",
             type=int,
             default=0,
-            help=_("server port (IP address or domain, default: use localhost)"),
-        )
-
-    def _setParamCb(self):
-        self.host.bridge.setParam(
-            "Password",
-            self.args.password,
-            "Connection",
-            profile_key=self.args.profile,
-            callback=self.host.quit,
-            errback=self.errback,
-        )
-
-    def _session_started(self, __):
-        self.host.bridge.setParam(
-            "JabberID",
-            self.args.jid,
-            "Connection",
-            profile_key=self.args.profile,
-            callback=self._setParamCb,
-            errback=self.errback,
+            help=_(f"server port (default: {C.XMPP_C2S_PORT})"),
         )
 
-    def _profileCreateCb(self):
-        self.disp(_("profile created"), 1)
-        self.host.bridge.profileStartSession(
-            self.args.password,
-            self.args.profile,
-            callback=self._session_started,
-            errback=self.errback,
-        )
+    async def start(self):
+        try:
+            await self.host.bridge.inBandAccountNew(
+                self.args.jid,
+                self.args.password,
+                self.args.email,
+                self.args.host,
+                self.args.port,
+            )
+        except Exception as e:
+            self.disp(
+                f"can't create account on {self.args.host or 'localhost'!r} with jid "
+                f"{self.args.jid!r} using In-Band Registration: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def _profileCreateEb(self, failure_):
-        self.disp(
-            _(
-                "Can't create profile {profile} to associate with jid {jid}: {msg}"
-            ).format(profile=self.args.profile, jid=self.args.jid, msg=failure_),
-            error=True,
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        self.disp(_("XMPP account created"), 1)
+
+        if self.args.profile is None:
+            self.host.quit()
 
-    def accountNewCb(self):
-        self.disp(_("XMPP account created"), 1)
-        if self.args.profile is not None:
-            self.disp(_("creating profile"), 2)
-            self.host.bridge.profileCreate(
+
+        self.disp(_("creating profile"), 2)
+        try:
+            await self.host.bridge.profileCreate(
                 self.args.profile,
                 self.args.password,
                 "",
-                callback=self._profileCreateCb,
-                errback=self._profileCreateEb,
+            )
+        except Exception as e:
+            self.disp(
+                _(f"Can't create profile {self.args.profile} to associate with jid "
+                  f"{self.args.jid}: {e}"),
+                error=True,
+            )
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        self.disp(_("profile created"), 1)
+        try:
+            await self.host.bridge.profileStartSession(
+                self.args.password,
+                self.args.profile,
             )
-        else:
-            self.host.quit()
+        except Exception as e:
+            self.disp(f"can't start profile session: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def accountNewEb(self, failure_):
+        try:
+            await self.host.bridge.setParam(
+                "JabberID",
+                self.args.jid,
+                "Connection",
+                profile_key=self.args.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set JabberID parameter: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        try:
+            await self.host.bridge.setParam(
+                "Password",
+                self.args.password,
+                "Connection",
+                profile_key=self.args.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set Password parameter: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
         self.disp(
-            _("Can't create new account on server {host} with jid {jid}: {msg}").format(
-                host=self.args.host or "localhost", jid=self.args.jid, msg=failure_
-            ),
-            error=True,
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.inBandAccountNew(
-            self.args.jid,
-            self.args.password,
-            self.args.email,
-            self.args.host,
-            self.args.port,
-            callback=self.accountNewCb,
-            errback=self.accountNewEb,
-        )
+            f"profile {self.args.profile} successfully created and associated to the new "
+            f"account", 1)
+        self.host.quit()
 
 
 class AccountModify(base.CommandBase):
@@ -153,20 +153,23 @@
         super(AccountModify, self).__init__(
             host, "modify", help=_("change password for XMPP account")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
             "password", help=_("new XMPP password")
         )
 
-    def start(self):
-        self.host.bridge.inBandPasswordChange(
-            self.args.password,
-            self.args.profile,
-            callback=self.host.quit,
-            errback=self.errback,
-        )
+    async def start(self):
+        try:
+            await self.host.bridge.inBandPasswordChange(
+                self.args.password,
+                self.args.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't change XMPP password: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class AccountDelete(base.CommandBase):
@@ -174,7 +177,6 @@
         super(AccountDelete, self).__init__(
             host, "delete", help=_("delete a XMPP account")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -184,32 +186,33 @@
             help=_("delete account without confirmation"),
         )
 
-    def _got_jid(self, jid_str):
+    async def start(self):
+        try:
+            jid_str = await self.host.bridge.asyncGetParamA(
+                "JabberID",
+                "Connection",
+                profile_key=self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get JID of the profile: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
         jid_ = jid.JID(jid_str)
         if not self.args.force:
             message = (
-                "You are about to delete the XMPP account with jid {jid_}\n"
-                'This is the XMPP account of profile "{profile}"\n'
-                "Are you sure that you want to delete this account ?".format(
-                    jid_=jid_, profile=self.profile
-                )
+                f"You are about to delete the XMPP account with jid {jid_!r}\n"
+                f"This is the XMPP account of profile {self.profile!r}\n"
+                f"Are you sure that you want to delete this account?"
             )
-            res = input("{} (y/N)? ".format(message))
-            if res not in ("y", "Y"):
-                self.disp(_("Account deletion cancelled"))
-                self.host.quit(2)
-        self.host.bridge.inBandUnregister(
-            jid_.domain, self.args.profile, callback=self.host.quit, errback=self.errback
-        )
+            await self.host.confirmOrQuit(message, _("Account deletion cancelled"))
 
-    def start(self):
-        self.host.bridge.asyncGetParamA(
-            "JabberID",
-            "Connection",
-            profile_key=self.profile,
-            callback=self._got_jid,
-            errback=self.errback,
-        )
+        try:
+            await self.host.bridge.inBandUnregister(jid_.domain, self.args.profile)
+        except Exception as e:
+            self.disp(f"can't delete XMPP account with jid {jid_!r}: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        self.host.quit()
 
 
 class Account(base.CommandBase):
--- a/sat_frontends/jp/cmd_adhoc.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_adhoc.py	Wed Sep 25 08:56:41 2019 +0200
@@ -19,7 +19,6 @@
 
 from . import base
 from sat.core.i18n import _
-from functools import partial
 from sat_frontends.jp.constants import Const as C
 from sat_frontends.jp import xmlui_manager
 
@@ -67,7 +66,7 @@
             "-l", "--loop", action="store_true", help=_("loop on the commands")
         )
 
-    def start(self):
+    async def start(self):
         name = self.args.software.lower()
         flags = []
         magics = {jid for jid in self.args.jids if jid.count("@") > 1}
@@ -75,29 +74,33 @@
         jids = set(self.args.jids).difference(magics)
         if self.args.loop:
             flags.append(FLAG_LOOP)
-        bus_name, methods = self.host.bridge.adHocDBusAddAuto(
-            name,
-            jids,
-            self.args.groups,
-            magics,
-            self.args.forbidden_jids,
-            self.args.forbidden_groups,
-            flags,
-            self.profile,
-        )
-        if not bus_name:
-            self.disp(_("No bus name found"), 1)
-            return
-        self.disp(_("Bus name found: [%s]" % bus_name), 1)
-        for method in methods:
-            path, iface, command = method
-            self.disp(
-                _(
-                    "Command found: (path:%(path)s, iface: %(iface)s) [%(command)s]"
-                    % {"path": path, "iface": iface, "command": command}
-                ),
-                1,
+        try:
+            bus_name, methods = await self.host.bridge.adHocDBusAddAuto(
+                name,
+                list(jids),
+                self.args.groups,
+                magics,
+                self.args.forbidden_jids,
+                self.args.forbidden_groups,
+                flags,
+                self.profile,
             )
+        except Exception as e:
+            self.disp(f"can't create remote control: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            if not bus_name:
+                self.disp(_("No bus name found"), 1)
+                self.host.quit(C.EXIT_NOT_FOUND)
+            else:
+                self.disp(_("Bus name found: [%s]" % bus_name), 1)
+                for method in methods:
+                    path, iface, command = method
+                    self.disp(
+                        _(f"Command found: (path:{path}, iface: {iface}) [{command}]"),
+                        1,
+                    )
+                self.host.quit()
 
 
 class Run(base.CommandBase):
@@ -107,7 +110,6 @@
         super(Run, self).__init__(
             host, "run", use_verbose=True, help=_("run an Ad-Hoc command")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -140,28 +142,24 @@
             help=_("node of the command (default: list commands)"),
         )
 
-    def adHocRunCb(self, xmlui_raw):
-        xmlui = xmlui_manager.create(self.host, xmlui_raw)
-        workflow = self.args.workflow
-        xmlui.show(workflow)
-        if not workflow:
-            if xmlui.type == "form":
-                xmlui.submitForm()
-            else:
-                self.host.quit()
-
-    def start(self):
-        self.host.bridge.adHocRun(
-            self.args.jid,
-            self.args.node,
-            self.profile,
-            callback=self.adHocRunCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get ad-hoc commands list: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            xmlui_raw = await self.host.bridge.adHocRun(
+                self.args.jid,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get ad-hoc commands list: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            xmlui = xmlui_manager.create(self.host, xmlui_raw)
+            workflow = self.args.workflow
+            await xmlui.show(workflow)
+            if not workflow:
+                if xmlui.type == "form":
+                    await xmlui.submitForm()
+            self.host.quit()
 
 
 class List(base.CommandBase):
@@ -171,7 +169,6 @@
         super(List, self).__init__(
             host, "list", use_verbose=True, help=_("list Ad-Hoc commands of a service")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -181,23 +178,19 @@
             help=_("jid of the service (default: profile's server"),
         )
 
-    def adHocListCb(self, xmlui_raw):
-        xmlui = xmlui_manager.create(self.host, xmlui_raw)
-        xmlui.readonly = True
-        xmlui.show()
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.adHocList(
-            self.args.jid,
-            self.profile,
-            callback=self.adHocListCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get ad-hoc commands list: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            xmlui_raw = await self.host.bridge.adHocList(
+                self.args.jid,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get ad-hoc commands list: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            xmlui = xmlui_manager.create(self.host, xmlui_raw)
+            await xmlui.show(read_only=True)
+            self.host.quit()
 
 
 class AdHoc(base.CommandBase):
--- a/sat_frontends/jp/cmd_avatar.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_avatar.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,59 +18,24 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-from . import base
 import os
 import os.path
+import asyncio
+from . import base
 from sat.core.i18n import _
 from sat_frontends.jp.constants import Const as C
 from sat.tools import config
-import subprocess
 
 
 __commands__ = ["Avatar"]
 DISPLAY_CMD = ["xv", "display", "gwenview", "showtell"]
 
 
-class Set(base.CommandBase):
-    def __init__(self, host):
-        super(Set, self).__init__(
-            host, "set", use_verbose=True, help=_("set avatar of the profile")
-        )
-        self.need_loop = True
-
-    def add_parser_options(self):
-        self.parser.add_argument(
-            "image_path", type=str, help=_("path to the image to upload")
-        )
-
-    def start(self):
-        """Send files to jabber contact"""
-        path = self.args.image_path
-        if not os.path.exists(path):
-            self.disp(_("file [{}] doesn't exist !").format(path), error=True)
-            self.host.quit(1)
-        path = os.path.abspath(path)
-        self.host.bridge.avatarSet(
-            path, self.profile, callback=self._avatarCb, errback=self._avatarEb
-        )
-
-    def _avatarCb(self):
-        self.disp(_("avatar has been set"), 1)
-        self.host.quit()
-
-    def _avatarEb(self, failure_):
-        self.disp(
-            _("error while uploading avatar: {msg}").format(msg=failure_), error=True
-        )
-        self.host.quit(C.EXIT_ERROR)
-
-
 class Get(base.CommandBase):
     def __init__(self, host):
         super(Get, self).__init__(
             host, "get", use_verbose=True, help=_("retrieve avatar of an entity")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument("jid", help=_("entity"))
@@ -78,54 +43,81 @@
             "-s", "--show", action="store_true", help=_("show avatar")
         )
 
-    def showImage(self, path):
+    async def showImage(self, path):
         sat_conf = config.parseMainConf()
         cmd = config.getConfig(sat_conf, "jp", "image_cmd")
         cmds = [cmd] + DISPLAY_CMD if cmd else DISPLAY_CMD
         for cmd in cmds:
             try:
-                ret = subprocess.call([cmd] + [path])
+                process = await asyncio.create_subprocess_exec(cmd, path)
+                ret = await process.wait()
             except OSError:
-                pass
-            else:
-                if ret in (0, 2):
-                    # we can get exit code 2 with display when stopping it with C-c
-                    break
+                continue
+
+            if ret in (0, 2):
+                # we can get exit code 2 with display when stopping it with C-c
+                break
         else:
             # didn't worked with commands, we try our luck with webbrowser
-            # in some cases, webbrowser can actually open the associated display program
+            # in some cases, webbrowser can actually open the associated display program.
+            # Note that this may be possibly blocking, depending on the platform and
+            # available browser
             import webbrowser
 
             webbrowser.open(path)
 
-    def _avatarGetCb(self, avatar_path):
+    async def start(self):
+        try:
+            avatar_path = await self.host.bridge.avatarGet(
+                self.args.jid,
+                False,
+                False,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't retrieve avatar: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
         if not avatar_path:
             self.disp(_("No avatar found."), 1)
             self.host.quit(C.EXIT_NOT_FOUND)
 
         self.disp(avatar_path)
         if self.args.show:
-            self.showImage(avatar_path)
+            await self.showImage(avatar_path)
 
         self.host.quit()
 
-    def _avatarGetEb(self, failure_):
-        self.disp(_("error while getting avatar: {msg}").format(msg=failure_), error=True)
-        self.host.quit(C.EXIT_ERROR)
+
+class Set(base.CommandBase):
+    def __init__(self, host):
+        super(Set, self).__init__(
+            host, "set", use_verbose=True, help=_("set avatar of the profile")
+        )
+
+    def add_parser_options(self):
+        self.parser.add_argument(
+            "image_path", type=str, help=_("path to the image to upload")
+        )
 
-    def start(self):
-        self.host.bridge.avatarGet(
-            self.args.jid,
-            False,
-            False,
-            self.profile,
-            callback=self._avatarGetCb,
-            errback=self._avatarGetEb,
-        )
+    async def start(self):
+        path = self.args.image_path
+        if not os.path.exists(path):
+            self.disp(_(f"file {path!r} doesn't exist!"), error=True)
+            self.host.quit(C.EXIT_BAD_ARG)
+        path = os.path.abspath(path)
+        try:
+            await self.host.bridge.avatarSet(path, self.profile)
+        except Exception as e:
+            self.disp(f"can't set avatar: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("avatar has been set"), 1)
+            self.host.quit()
 
 
 class Avatar(base.CommandBase):
-    subcommands = (Set, Get)
+    subcommands = (Get, Set)
 
     def __init__(self, host):
         super(Avatar, self).__init__(
--- a/sat_frontends/jp/cmd_blog.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_blog.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,6 +18,16 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+import json
+import sys
+import os.path
+import os
+import time
+import tempfile
+import subprocess
+import asyncio
+from asyncio.subprocess import DEVNULL
+from pathlib import Path
 from . import base
 from sat.core.i18n import _
 from sat_frontends.jp.constants import Const as C
@@ -27,15 +37,6 @@
 from sat.tools.common import uri
 from sat.tools import config
 from configparser import NoSectionError, NoOptionError
-from functools import partial
-import json
-import sys
-import os.path
-import os
-import time
-import tempfile
-import subprocess
-import codecs
 from sat.tools.common import data_format
 
 __commands__ = ["Blog"]
@@ -64,7 +65,7 @@
 )
 
 URL_REDIRECT_PREFIX = "url_redirect_"
-INOTIFY_INSTALL = '"pip install inotify"'
+AIONOTIFY_INSTALL = '"pip install aionotify"'
 MB_KEYS = (
     "id",
     "url",
@@ -86,7 +87,7 @@
 OUTPUT_OPT_NO_HEADER = "no-header"
 
 
-def guessSyntaxFromPath(host, sat_conf, path):
+async def guessSyntaxFromPath(host, sat_conf, path):
     """Return syntax guessed according to filename extension
 
     @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
@@ -101,19 +102,35 @@
                 return k
 
     # if not found, we use current syntax
-    return host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
+    return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
 
 
 class BlogPublishCommon(object):
     """handle common option for publising commands (Set and Edit)"""
 
-    @property
-    def current_syntax(self):
-        if self._current_syntax is None:
-            self._current_syntax = self.host.bridge.getParamA(
+    async def get_current_syntax(self):
+        """Retrieve current_syntax
+
+        Use default syntax if --syntax has not been used, else check given syntax.
+        Will set self.default_syntax_used to True if default syntax has been used
+        """
+        if self.args.syntax is None:
+            self.default_syntax_used = True
+            return await self.host.bridge.getParamA(
                 "Syntax", "Composition", "value", self.profile
             )
-        return self._current_syntax
+        else:
+            self.default_syntax_used = False
+            try:
+                syntax = await self.host.bridge.syntaxGet(self.current_syntax)
+
+                self.current_syntax = self.args.syntax = syntax
+            except Exception as e:
+                if e.classname == "NotFound":
+                    self.parser.error(_(f"unknown syntax requested ({self.args.syntax})"))
+                else:
+                    raise e
+        return self.args.syntax
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -143,14 +160,14 @@
             help=_("syntax to use (default: get profile's default syntax)"),
         )
 
-    def setMbDataContent(self, content, mb_data):
-        if self.args.syntax is None:
+    async def setMbDataContent(self, content, mb_data):
+        if self.default_syntax_used:
             # default syntax has been used
             mb_data["content_rich"] = content
         elif self.current_syntax == SYNTAX_XHTML:
             mb_data["content_xhtml"] = content
         else:
-            mb_data["content_xhtml"] = self.host.bridge.syntaxConvert(
+            mb_data["content_xhtml"] = await self.host.bridge.syntaxConvert(
                 content, self.current_syntax, SYNTAX_XHTML, False, self.profile
             )
 
@@ -178,37 +195,35 @@
             help=_("publish a new blog item or update an existing one"),
         )
         BlogPublishCommon.__init__(self)
-        self.need_loop = True
 
     def add_parser_options(self):
         BlogPublishCommon.add_parser_options(self)
 
-    def mbSendCb(self):
-        self.disp("Item published")
-        self.host.quit(C.EXIT_OK)
-
-    def start(self):
-        self._current_syntax = self.args.syntax
+    async def start(self):
+        self.current_syntax = await self.get_current_syntax()
         self.pubsub_item = self.args.item
         mb_data = {}
         self.setMbDataFromArgs(mb_data)
         if self.pubsub_item:
             mb_data["id"] = self.pubsub_item
-        content = codecs.getreader("utf-8")(sys.stdin).read()
-        self.setMbDataContent(content, mb_data)
+        content = sys.stdin.read()
+        await self.setMbDataContent(content, mb_data)
 
-        self.host.bridge.mbSend(
-            self.args.service,
-            self.args.node,
-            data_format.serialise(mb_data),
-            self.profile,
-            callback=self.exitCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't send item: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.mbSend(
+                self.args.service,
+                self.args.node,
+                data_format.serialise(mb_data),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(
+                f"can't send item: {e}", error=True
+            )
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp("Item published")
+            self.host.quit(C.EXIT_OK)
 
 
 class Get(base.CommandBase):
@@ -227,7 +242,6 @@
             extra_outputs=extra_outputs,
             help=_("get blog item(s)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         #  TODO: a key(s) argument to select keys to display
@@ -401,28 +415,25 @@
 
             print(("\n" + sep + "\n"))
 
-    def mbGetCb(self, mb_result):
-        items, metadata = mb_result
-        items = [data_format.deserialise(i) for i in items]
-        mb_result = items, metadata
-        self.output(mb_result)
-        self.host.quit(C.EXIT_OK)
-
-    def mbGetEb(self, failure_):
-        self.disp("can't get blog items: {reason}".format(reason=failure_), error=True)
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.mbGet(
-            self.args.service,
-            self.args.node,
-            self.args.max,
-            self.args.items,
-            self.getPubsubExtra(),
-            self.profile,
-            callback=self.mbGetCb,
-            errback=self.mbGetEb,
-        )
+    async def start(self):
+        try:
+            mb_result = await self.host.bridge.mbGet(
+                self.args.service,
+                self.args.node,
+                self.args.max,
+                self.args.items,
+                self.getPubsubExtra(),
+                self.profile
+            )
+        except Exception as e:
+            self.disp(f"can't get blog items: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            items, metadata = mb_result
+            items = [data_format.deserialise(i) for i in items]
+            mb_result = items, metadata
+            await self.output(mb_result)
+            self.host.quit(C.EXIT_OK)
 
 
 class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
@@ -452,25 +463,27 @@
     def buildMetadataFile(self, content_file_path, mb_data=None):
         """Build a metadata file using json
 
-        The file is named after content_file_path, with extension replaced by _metadata.json
-        @param content_file_path(str): path to the temporary file which will contain the body
+        The file is named after content_file_path, with extension replaced by
+        _metadata.json
+        @param content_file_path(str): path to the temporary file which will contain the
+            body
         @param mb_data(dict, None): microblog metadata (for existing items)
-        @return (tuple[dict, str]): merged metadata put originaly in metadata file
+        @return (tuple[dict, Path]): merged metadata put originaly in metadata file
             and path to temporary metadata file
         """
         # we first construct metadata from edited item ones and CLI argumments
         # or re-use the existing one if it exists
-        meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF
-        if os.path.exists(meta_file_path):
+        meta_file_path = content_file_path.with_name(
+            content_file_path.stem + common.METADATA_SUFF)
+        if meta_file_path.exists():
             self.disp("Metadata file already exists, we re-use it")
             try:
-                with open(meta_file_path, "rb") as f:
+                with meta_file_path.open("rb") as f:
                     mb_data = json.load(f)
             except (OSError, IOError, ValueError) as e:
                 self.disp(
-                    "Can't read existing metadata file at {path}, aborting: {reason}".format(
-                        path=meta_file_path, reason=e
-                    ),
+                    f"Can't read existing metadata file at {meta_file_path}, "
+                    f"aborting: {e}",
                     error=True,
                 )
                 self.host.quit(1)
@@ -491,7 +504,8 @@
         with os.fdopen(
             os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b"
         ) as f:
-            # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters
+            # we need to use an intermediate unicode buffer to write to the file
+            # unicode without escaping characters
             unicode_dump = json.dumps(
                 mb_data,
                 ensure_ascii=False,
@@ -503,19 +517,20 @@
 
         return mb_data, meta_file_path
 
-    def edit(self, content_file_path, content_file_obj, mb_data=None):
+    async def edit(self, content_file_path, content_file_obj, mb_data=None):
         """Edit the file contening the content using editor, and publish it"""
         # we first create metadata file
         meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data)
 
+        coroutines = []
+
         # do we need a preview ?
         if self.args.preview:
             self.disp("Preview requested, launching it", 1)
             # we redirect outputs to /dev/null to avoid console pollution in editor
             # if user wants to see messages, (s)he can call "blog preview" directly
-            DEVNULL = open(os.devnull, "wb")
-            subprocess.Popen(
-                [
+            coroutines.append(
+                asyncio.create_subprocess_exec(
                     sys.argv[0],
                     "blog",
                     "preview",
@@ -523,30 +538,34 @@
                     "true",
                     "-p",
                     self.profile,
-                    content_file_path,
-                ],
-                stdout=DEVNULL,
-                stderr=subprocess.STDOUT,
+                    str(content_file_path),
+                    stdout=DEVNULL,
+                    stderr=DEVNULL,
+                )
             )
 
         # we launch editor
-        self.runEditor(
-            "blog_editor_args",
-            content_file_path,
-            content_file_obj,
-            meta_file_path=meta_file_path,
-            meta_ori=meta_ori,
+        coroutines.append(
+            self.runEditor(
+                "blog_editor_args",
+                content_file_path,
+                content_file_obj,
+                meta_file_path=meta_file_path,
+                meta_ori=meta_ori,
+            )
         )
 
-    def publish(self, content, mb_data):
-        self.setMbDataContent(content, mb_data)
+        await asyncio.gather(*coroutines)
+
+    async def publish(self, content, mb_data):
+        await self.setMbDataContent(content, mb_data)
 
         if self.pubsub_item:
             mb_data["id"] = self.pubsub_item
 
         mb_data = data_format.serialise(mb_data)
 
-        self.host.bridge.mbSend(
+        await self.host.bridge.mbSend(
             self.pubsub_service, self.pubsub_node, mb_data, self.profile
         )
         self.disp("Blog item published")
@@ -555,22 +574,27 @@
         # we get current syntax to determine file extension
         return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[""])
 
-    def getItemData(self, service, node, item):
+    async def getItemData(self, service, node, item):
         items = [item] if item else []
-        mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0]
-        mb_data = data_format.deserialise(mb_data)
+
+        mb_data = await self.host.bridge.mbGet(
+            service, node, 1, items, {}, self.profile)
+        mb_data = data_format.deserialise(mb_data[0][0])
+
         try:
             content = mb_data["content_xhtml"]
         except KeyError:
             content = mb_data["content"]
             if content:
-                content = self.host.bridge.syntaxConvert(
+                content = await self.host.bridge.syntaxConvert(
                     content, "text", SYNTAX_XHTML, False, self.profile
                 )
+
         if content and self.current_syntax != SYNTAX_XHTML:
-            content = self.host.bridge.syntaxConvert(
+            content = await self.host.bridge.syntaxConvert(
                 content, SYNTAX_XHTML, self.current_syntax, False, self.profile
             )
+
         if content and self.current_syntax == SYNTAX_XHTML:
             content = content.strip()
             if not content.startswith('<div>'):
@@ -586,37 +610,16 @@
 
         return content, mb_data, mb_data["id"]
 
-    def start(self):
+    async def start(self):
         # if there are user defined extension, we use them
         SYNTAX_EXT.update(config.getConfig(self.sat_conf, "jp", CONF_SYNTAX_EXT, {}))
-        self._current_syntax = self.args.syntax
-        if self._current_syntax is not None:
-            try:
-                self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(
-                    self.current_syntax
-                )
-            except Exception as e:
-                if "NotFound" in str(
-                    e
-                ):  #  FIXME: there is not good way to check bridge errors
-                    self.parser.error(
-                        _("unknown syntax requested ({syntax})").format(
-                            syntax=self.args.syntax
-                        )
-                    )
-                else:
-                    raise e
+        self.current_syntax = await self.get_current_syntax()
 
-        (
-            self.pubsub_service,
-            self.pubsub_node,
-            self.pubsub_item,
-            content_file_path,
-            content_file_obj,
-            mb_data,
-        ) = self.getItemPath()
+        (self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path,
+         content_file_obj, mb_data,) = await self.getItemPath()
 
-        self.edit(content_file_path, content_file_obj, mb_data=mb_data)
+        await self.edit(content_file_path, content_file_obj, mb_data=mb_data)
+        self.host.quit()
 
 
 class Preview(base.CommandBase, common.BaseEdit):
@@ -643,14 +646,14 @@
             help=_("path to the content file"),
         )
 
-    def showPreview(self):
+    async def showPreview(self):
         # we implement showPreview here so we don't have to import webbrowser and urllib
         # when preview is not used
-        url = "file:{}".format(self.urllib.quote(self.preview_file_path))
+        url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path))
         self.webbrowser.open_new_tab(url)
 
-    def _launchPreviewExt(self, cmd_line, opt_name):
-        url = "file:{}".format(self.urllib.quote(self.preview_file_path))
+    async def _launchPreviewExt(self, cmd_line, opt_name):
+        url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path))
         args = common.parse_args(
             self.host, cmd_line, url=url, preview_file=self.preview_file_path
         )
@@ -662,34 +665,34 @@
             self.host.quit(1)
         subprocess.Popen(args)
 
-    def openPreviewExt(self):
-        self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd")
+    async def openPreviewExt(self):
+        await self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd")
 
-    def updatePreviewExt(self):
-        self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd")
+    async def updatePreviewExt(self):
+        await self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd")
 
-    def updateContent(self):
-        with open(self.content_file_path, "rb") as f:
+    async def updateContent(self):
+        with self.content_file_path.open("rb") as f:
             content = f.read().decode("utf-8-sig")
             if content and self.syntax != SYNTAX_XHTML:
                 # we use safe=True because we want to have a preview as close as possible
                 # to what the people will see
-                content = self.host.bridge.syntaxConvert(
+                content = await self.host.bridge.syntaxConvert(
                     content, self.syntax, SYNTAX_XHTML, True, self.profile
                 )
 
         xhtml = (
-            '<html xmlns="http://www.w3.org/1999/xhtml">'
-            '<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
-            "</head>"
-            "<body>{}</body>"
-            "</html>"
-        ).format(content)
+            f'<html xmlns="http://www.w3.org/1999/xhtml">'
+            f'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
+            f'</head>'
+            f'<body>{content}</body>'
+            f'</html>'
+        )
 
         with open(self.preview_file_path, "wb") as f:
             f.write(xhtml.encode("utf-8"))
 
-    def start(self):
+    async def start(self):
         import webbrowser
         import urllib.request, urllib.parse, urllib.error
 
@@ -697,33 +700,24 @@
 
         if self.args.inotify != "false":
             try:
-                import inotify.adapters
-                import inotify.constants
-                from inotify.calls import InotifyError
+                import aionotify
+
             except ImportError:
                 if self.args.inotify == "auto":
-                    inotify = None
+                    aionotify = None
                     self.disp(
-                        "inotify module not found, deactivating feature. You can install"
-                        " it with {install}".format(install=INOTIFY_INSTALL)
+                        f"aionotify module not found, deactivating feature. You can "
+                        f"install it with {AIONOTIFY_INSTALL}"
                     )
                 else:
                     self.disp(
-                        "inotify not found, can't activate the feature! Please install "
-                        "it with {install}".format(install=INOTIFY_INSTALL),
+                        f"aioinotify not found, can't activate the feature! Please "
+                        f"install it with {AIONOTIFY_INSTALL}",
                         error=True,
                     )
                     self.host.quit(1)
-            else:
-                # we deactivate logging in inotify, which is quite annoying
-                try:
-                    inotify.adapters._LOGGER.setLevel(40)
-                except AttributeError:
-                    self.disp(
-                        "Logger doesn't exists, inotify may have chanded", error=True
-                    )
         else:
-            inotify = None
+            aionotify = None
 
         sat_conf = config.parseMainConf()
         SYNTAX_EXT.update(config.getConfig(sat_conf, "jp", CONF_SYNTAX_EXT, {}))
@@ -750,76 +744,89 @@
         if self.args.file == "current":
             self.content_file_path = self.getCurrentFile(self.profile)
         else:
-            self.content_file_path = os.path.abspath(self.args.file)
+            try:
+                self.content_file_path = Path(self.args.file).resolve(strict=True)
+            except FileNotFoundError:
+                self.disp(_(f'File "{self.args.file}" doesn\'t exist!'))
+                self.host.quit(C.EXIT_NOT_FOUND)
 
-        self.syntax = guessSyntaxFromPath(self.host, sat_conf, self.content_file_path)
+        self.syntax = await guessSyntaxFromPath(
+            self.host, sat_conf, self.content_file_path)
 
         # at this point the syntax is converted, we can display the preview
         preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False)
         self.preview_file_path = preview_file.name
         preview_file.close()
-        self.updateContent()
+        await self.updateContent()
 
-        if inotify is None:
-            # XXX: we don't delete file automatically because browser need it
+        if aionotify is None:
+            # XXX: we don't delete file automatically because browser needs it
             #      (and webbrowser.open can return before it is read)
             self.disp(
-                "temporary file created at {}\nthis file will NOT BE DELETED "
-                "AUTOMATICALLY, please delete it yourself when you have finished".format(
-                    self.preview_file_path
-                )
+                f"temporary file created at {self.preview_file_path}\nthis file will NOT "
+                f"BE DELETED AUTOMATICALLY, please delete it yourself when you have "
+                f"finished"
             )
-            open_cb()
+            await open_cb()
         else:
-            open_cb()
-            i = inotify.adapters.Inotify(
-                block_duration_s=60
-            )  # no need for 1 s duraction, inotify drive actions here
+            await open_cb()
+            watcher = aionotify.Watcher()
+            watcher_kwargs = {
+                # Watcher don't accept Path so we convert to string
+                "path": str(self.content_file_path),
+                "alias": 'content_file',
+                "flags": aionotify.Flags.CLOSE_WRITE
+                         | aionotify.Flags.DELETE_SELF
+                         | aionotify.Flags.MOVE_SELF,
+            }
+            watcher.watch(**watcher_kwargs)
 
-            def add_watch():
-                i.add_watch(
-                    self.content_file_path.encode('utf-8'),
-                    mask=inotify.constants.IN_CLOSE_WRITE
-                    | inotify.constants.IN_DELETE_SELF
-                    | inotify.constants.IN_MOVE_SELF,
-                )
-
-            add_watch()
+            loop = asyncio.get_event_loop()
+            await watcher.setup(loop)
 
             try:
-                for event in i.event_gen():
-                    if event is not None:
-                        self.disp("Content updated", 1)
-                        if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]):
+                while True:
+                    event = await watcher.get_event()
+                    self.disp("Content updated", 1)
+                    if event.flags & (aionotify.Flags.DELETE_SELF
+                                      | aionotify.Flags.MOVE_SELF):
+                        self.disp(
+                            "DELETE/MOVE event catched, changing the watch",
+                            2,
+                        )
+                        try:
+                            watcher.unwatch('content_file')
+                        except IOError as e:
                             self.disp(
-                                "{} event catched, changing the watch".format(
-                                    ", ".join(event[1])
-                                ),
+                                f"Can't remove the watch: {e}",
                                 2,
                             )
-                            i.remove_watch(self.content_file_path)
-                            try:
-                                add_watch()
-                            except InotifyError:
-                                # if the new file is not here yet we can have an error
-                                # as a workaround, we do a little rest
-                                time.sleep(1)
-                                add_watch()
-                        self.updateContent()
-                        update_cb()
-            except InotifyError:
-                self.disp(
-                    "Can't catch inotify events, as the file been deleted?", error=True
-                )
+                        watcher = aionotify.Watcher()
+                        watcher.watch(**watcher_kwargs)
+                        try:
+                            await watcher.setup(loop)
+                        except OSError:
+                            # if the new file is not here yet we can have an error
+                            # as a workaround, we do a little rest and try again
+                            await asyncio.sleep(1)
+                            await watcher.setup(loop)
+                    await self.updateContent()
+                    await update_cb()
+            except FileNotFoundError:
+                self.disp("The file seems to have been deleted.", error=True)
+                self.host.quit(C.EXIT_NOT_FOUND)
             finally:
                 os.unlink(self.preview_file_path)
                 try:
-                    i.remove_watch(self.content_file_path)
-                except InotifyError:
-                    pass
+                    watcher.unwatch('content_file')
+                except IOError as e:
+                    self.disp(
+                        f"Can't remove the watch: {e}",
+                        2,
+                    )
 
 
-class Import(base.CommandAnswering):
+class Import(base.CommandBase):
     def __init__(self, host):
         super(Import, self).__init__(
             host,
@@ -828,7 +835,6 @@
             use_progress=True,
             help=_("import an external blog"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -871,10 +877,10 @@
             ),
         )
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("Blog upload started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("Blog uploaded successfully"), 2)
         redirections = {
             k[len(URL_REDIRECT_PREFIX) :]: v
@@ -897,42 +903,35 @@
             )
             self.disp(
                 _(
-                    "\nTo redirect old URLs to new ones, put the following lines in your"
-                    " sat.conf file, in [libervia] section:\n\n{conf}".format(conf=conf)
+                    f"\nTo redirect old URLs to new ones, put the following lines in your"
+                    f" sat.conf file, in [libervia] section:\n\n{conf}"
                 )
             )
 
-    def onProgressError(self, error_msg):
-        self.disp(_("Error while uploading blog: {}").format(error_msg), error=True)
+    async def onProgressError(self, error_msg):
+        self.disp(_(f"Error while uploading blog: {error_msg}"), error=True)
 
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to upload a blog: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def start(self):
+    async def start(self):
         if self.args.location is None:
             for name in ("option", "service", "no_images_upload"):
                 if getattr(self.args, name):
                     self.parser.error(
                         _(
-                            "{name} argument can't be used without location argument"
-                        ).format(name=name)
+                            f"{name} argument can't be used without location argument"
+                        )
                     )
             if self.args.importer is None:
                 self.disp(
                     "\n".join(
                         [
-                            "{}: {}".format(name, desc)
-                            for name, desc in self.host.bridge.blogImportList()
+                            f"{name}: {desc}"
+                            for name, desc in await self.host.bridge.blogImportList()
                         ]
                     )
                 )
             else:
                 try:
-                    short_desc, long_desc = self.host.bridge.blogImportDesc(
+                    short_desc, long_desc = await self.host.bridge.blogImportDesc(
                         self.args.importer
                     )
                 except Exception as e:
@@ -942,13 +941,7 @@
                     self.disp(msg)
                     self.host.quit(1)
                 else:
-                    self.disp(
-                        "{name}: {short_desc}\n\n{long_desc}".format(
-                            name=self.args.importer,
-                            short_desc=short_desc,
-                            long_desc=long_desc,
-                        )
-                    )
+                    self.disp(f"{self.args.importer}: {short_desc}\n\n{long_desc}")
             self.host.quit()
         else:
             # we have a location, an import is requested
@@ -967,20 +960,23 @@
             elif self.args.upload_ignore_host:
                 options["upload_ignore_host"] = self.args.upload_ignore_host
 
-            def gotId(id_):
-                self.progress_id = id_
+            try:
+                progress_id = await self.host.bridge.blogImport(
+                    self.args.importer,
+                    self.args.location,
+                    options,
+                    self.args.service,
+                    self.args.node,
+                    self.profile,
+                )
+            except Exception as e:
+                self.disp(
+                    _(f"Error while trying to import a blog: {e}"),
+                    error=True,
+                )
+                self.host.quit(1)
 
-            self.host.bridge.blogImport(
-                self.args.importer,
-                self.args.location,
-                options,
-                self.args.service,
-                self.args.node,
-                self.profile,
-                callback=gotId,
-                errback=self.error,
-            )
-
+            await self.set_progress_id(progress_id)
 
 class Blog(base.CommandBase):
     subcommands = (Set, Get, Edit, Preview, Import)
--- a/sat_frontends/jp/cmd_bookmarks.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_bookmarks.py	Wed Sep 25 08:56:41 2019 +0200
@@ -19,6 +19,7 @@
 
 from . import base
 from sat.core.i18n import _
+from sat_frontends.jp.constants import Const as C
 
 __commands__ = ["Bookmarks"]
 
@@ -32,23 +33,25 @@
         self.parser.add_argument('-l', '--location', type=str, choices=(location_default,) + STORAGE_LOCATIONS, default=location_default, help=_("storage location (default: %(default)s)"))
         self.parser.add_argument('-t', '--type', type=str, choices=TYPES, default=TYPES[0], help=_("bookmarks type (default: %(default)s)"))
 
-    def _errback(self, failure):
-        print((("Something went wrong: [%s]") % failure))
-        self.host.quit(1)
-
 class BookmarksList(BookmarksCommon):
 
     def __init__(self, host):
         super(BookmarksList, self).__init__(host, 'list', help=_('list bookmarks'))
 
-    def start(self):
-        data = self.host.bridge.bookmarksList(self.args.type, self.args.location, self.host.profile)
+    async def start(self):
+        try:
+            data = await self.host.bridge.bookmarksList(
+                self.args.type, self.args.location, self.host.profile)
+        except Exception as e:
+            self.disp(f"can't get bookmarks list: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
         mess = []
         for location in STORAGE_LOCATIONS:
             if not data[location]:
                 continue
             loc_mess = []
-            loc_mess.append("%s:" % location)
+            loc_mess.append(f"{location}:")
             book_mess = []
             for book_link, book_data in list(data[location].items()):
                 name = book_data.get('name')
@@ -62,41 +65,56 @@
             mess.append('\n'.join(loc_mess))
 
         print('\n\n'.join(mess))
+        self.host.quit()
 
 
 class BookmarksRemove(BookmarksCommon):
 
     def __init__(self, host):
         super(BookmarksRemove, self).__init__(host, 'remove', help=_('remove a bookmark'))
-        self.need_loop = True
 
     def add_parser_options(self):
         super(BookmarksRemove, self).add_parser_options()
-        self.parser.add_argument('bookmark', help=_('jid (for muc bookmark) or url of to remove'))
+        self.parser.add_argument(
+            'bookmark', help=_('jid (for muc bookmark) or url of to remove'))
+        self.parser.add_argument(
+            "-f", "--force", action="store_true",
+            help=_("delete bookmark without confirmation"),)
+
+    async def start(self):
+        if not self.args.force:
+            await self.host.confirmOrQuit(_("Are you sure to delete this bookmark?"))
 
-    def start(self):
-        self.host.bridge.bookmarksRemove(self.args.type, self.args.bookmark, self.args.location, self.host.profile, callback = lambda: self.host.quit(), errback=self._errback)
+        try:
+            await self.host.bridge.bookmarksRemove(
+                self.args.type, self.args.bookmark, self.args.location, self.host.profile)
+        except Exception as e:
+            self.disp(_(f"can't delete bookmark: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_('bookmark deleted'))
+            self.host.quit()
 
 
 class BookmarksAdd(BookmarksCommon):
 
     def __init__(self, host):
         super(BookmarksAdd, self).__init__(host, 'add', help=_('add a bookmark'))
-        self.need_loop = True
 
     def add_parser_options(self):
         super(BookmarksAdd, self).add_parser_options(location_default='auto')
-        self.parser.add_argument('bookmark', help=_('jid (for muc bookmark) or url of to remove'))
+        self.parser.add_argument(
+            'bookmark', help=_('jid (for muc bookmark) or url of to remove'))
         self.parser.add_argument('-n', '--name', help=_("bookmark name"))
         muc_group = self.parser.add_argument_group(_('MUC specific options'))
         muc_group.add_argument('-N', '--nick', help=_('nickname'))
-        muc_group.add_argument('-a', '--autojoin', action='store_true', help=_('join room on profile connection'))
+        muc_group.add_argument(
+            '-a', '--autojoin', action='store_true',
+            help=_('join room on profile connection'))
 
-    def start(self):
+    async def start(self):
         if self.args.type == 'url' and (self.args.autojoin or self.args.nick is not None):
-            # XXX: Argparse doesn't seem to manage this case, any better way ?
-            print(_("You can't use --autojoin or --nick with --type url"))
-            self.host.quit(1)
+            self.parser.error(_("You can't use --autojoin or --nick with --type url"))
         data = {}
         if self.args.autojoin:
             data['autojoin'] = 'true'
@@ -104,7 +122,16 @@
             data['nick'] = self.args.nick
         if self.args.name is not None:
             data['name'] = self.args.name
-        self.host.bridge.bookmarksAdd(self.args.type, self.args.bookmark, data, self.args.location, self.host.profile, callback = lambda: self.host.quit(), errback=self._errback)
+        try:
+            await self.host.bridge.bookmarksAdd(
+                self.args.type, self.args.bookmark, data, self.args.location,
+                self.host.profile)
+        except Exception as e:
+            self.disp(f"can't add bookmark: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_('bookmark successfully added'))
+            self.host.quit()
 
 
 class Bookmarks(base.CommandBase):
--- a/sat_frontends/jp/cmd_debug.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_debug.py	Wed Sep 25 08:56:41 2019 +0200
@@ -48,7 +48,6 @@
     def __init__(self, host):
         base.CommandBase.__init__(self, host, "method", help=_("call a bridge method"))
         BridgeCommon.__init__(self)
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -58,34 +57,31 @@
             "arg", nargs="*", help=_("argument of the method")
         )
 
-    def method_cb(self, ret=None):
-        if ret is not None:
-            self.disp(str(ret))
-        self.host.quit()
-
-    def method_eb(self, failure):
-        self.disp(
-            _("Error while executing {}: {}".format(self.args.method, failure)),
-            error=True,
-        )
-        self.host.quit(C.EXIT_ERROR)
-
-    def start(self):
+    async def start(self):
         method = getattr(self.host.bridge, self.args.method)
+        import inspect
+        argspec = inspect.getargspec(method)
+
+        kwargs = {}
+        if 'profile_key' in argspec.args:
+            kwargs['profile_key'] = self.profile
+        elif 'profile' in argspec.args:
+            kwargs['profile'] = self.profile
+
         args = self.evalArgs()
+
         try:
-            method(
+            ret = await method(
                 *args,
-                profile=self.profile,
-                callback=self.method_cb,
-                errback=self.method_eb
+                **kwargs,
             )
-        except TypeError:
-            # maybe the method doesn't need a profile ?
-            try:
-                method(*args, callback=self.method_cb, errback=self.method_eb)
-            except TypeError:
-                self.method_eb(_("bad arguments"))
+        except Exception as e:
+            self.disp(_(f"Error while executing {self.args.method}: {e}"), error=True)
+            self.host.quit(C.EXIT_ERROR)
+        else:
+            if ret is not None:
+                self.disp(str(ret))
+            self.host.quit()
 
 
 class Signal(base.CommandBase, BridgeCommon):
@@ -103,12 +99,18 @@
             "arg", nargs="*", help=_("argument of the signal")
         )
 
-    def start(self):
+    async def start(self):
         args = self.evalArgs()
         json_args = json.dumps(args)
         # XXX: we use self.args.profile and not self.profile
         #      because we want the raw profile_key (so plugin handle C.PROF_KEY_NONE)
-        self.host.bridge.debugFakeSignal(self.args.signal, json_args, self.args.profile)
+        try:
+            await self.host.bridge.debugFakeSignal(self.args.signal, json_args, self.args.profile)
+        except Exception as e:
+            self.disp(_(f"Can't send fake signal: {e}"), error=True)
+            self.host.quit(C.EXIT_ERROR)
+        else:
+            self.host.quit()
 
 
 class Bridge(base.CommandBase):
@@ -130,7 +132,6 @@
             use_output=C.OUTPUT_XML,
             help=_("monitor XML stream"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -141,7 +142,7 @@
             help=_("stream direction filter"),
         )
 
-    def printXML(self, direction, xml_data, profile):
+    async def printXML(self, direction, xml_data, profile):
         if self.args.direction == "in" and direction != "IN":
             return
         if self.args.direction == "out" and direction != "OUT":
@@ -155,7 +156,7 @@
             whiteping = False
 
         if verbosity:
-            profile_disp = " ({})".format(profile) if verbosity > 1 else ""
+            profile_disp = f" ({profile})" if verbosity > 1 else ""
             if direction == "IN":
                 self.disp(
                     A.color(
@@ -172,7 +173,7 @@
             self.disp("[WHITESPACE PING]")
         else:
             try:
-                self.output(xml_data)
+                await self.output(xml_data)
             except Exception:
                 #  initial stream is not valid XML,
                 # in this case we print directly to data
@@ -182,7 +183,7 @@
                 self.disp(xml_data)
                 self.disp("")
 
-    def start(self):
+    async def start(self):
         self.host.bridge.register_signal("xmlLog", self.printXML, "plugin")
 
 
--- a/sat_frontends/jp/cmd_encryption.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_encryption.py	Wed Sep 25 08:56:41 2019 +0200
@@ -20,7 +20,6 @@
 from sat_frontends.jp import base
 from sat_frontends.jp.constants import Const as C
 from sat.core.i18n import _
-from functools import partial
 from sat.tools.common import data_format
 from sat_frontends.jp import xmlui_manager
 
@@ -37,34 +36,27 @@
             extra_outputs=extra_outputs,
             use_profile=False,
             help=_("show available encryption algorithms"))
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def encryptionPluginsGetCb(self, plugins):
-        self.output(plugins)
-        self.host.quit()
-
     def default_output(self, plugins):
         if not plugins:
             self.disp(_("No encryption plugin registered!"))
-            self.host.quit(C.EXIT_NOT_FOUND)
         else:
             self.disp(_("Following encryption algorithms are available: {algos}").format(
                 algos=', '.join([p['name'] for p in plugins])))
+
+    async def start(self):
+        try:
+            plugins = await self.host.bridge.encryptionPluginsGet()
+        except Exception as e:
+            self.disp(f"can't retrieve plugins: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(plugins)
             self.host.quit()
 
-    def start(self):
-        self.host.bridge.encryptionPluginsGet(
-            callback=self.encryptionPluginsGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't retrieve plugins: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
 
 class EncryptionGet(base.CommandBase):
 
@@ -73,7 +65,6 @@
             host, "get",
             use_output=C.OUTPUT_DICT,
             help=_("get encryption session data"))
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -81,28 +72,23 @@
             help=_("jid of the entity to check")
         )
 
-    def messageEncryptionGetCb(self, serialised):
+    async def start(self):
+        jids = await self.host.check_jids([self.args.jid])
+        jid = jids[0]
+        try:
+            serialised = await self.host.bridge.messageEncryptionGet(jid, self.profile)
+        except Exception as e:
+            self.disp(f"can't get session: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
         session_data = data_format.deserialise(serialised)
         if session_data is None:
             self.disp(
                 "No encryption session found, the messages are sent in plain text.")
             self.host.quit(C.EXIT_NOT_FOUND)
-        self.output(session_data)
+        await self.output(session_data)
         self.host.quit()
 
-    def start(self):
-        jids = self.host.check_jids([self.args.jid])
-        jid = jids[0]
-        self.host.bridge.messageEncryptionGet(
-            jid, self.profile,
-            callback=self.messageEncryptionGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get session: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
 
 class EncryptionStart(base.CommandBase):
 
@@ -110,7 +96,6 @@
         super(EncryptionStart, self).__init__(
             host, "start",
             help=_("start encrypted session with an entity"))
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -128,30 +113,30 @@
             help=_("jid of the entity to stop encrypted session with")
         )
 
-    def encryptionNamespaceGetCb(self, namespace):
-        jids = self.host.check_jids([self.args.jid])
-        jid = jids[0]
-        self.host.bridge.messageEncryptionStart(
-            jid, namespace, not self.args.encrypt_noreplace,
-            self.profile,
-            callback=self.host.quit,
-            errback=partial(self.errback,
-                            msg=_("Can't start encryption session: {}"),
-                            exit_code=C.EXIT_BRIDGE_ERRBACK,
-                            ))
+    async def start(self):
+        if self.args.name is not None:
+            try:
+                namespace = await self.host.bridge.encryptionNamespaceGet(self.args.name)
+            except Exception as e:
+                self.disp(f"can't get encryption namespace: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        elif self.args.namespace is not None:
+            namespace = self.args.namespace
+        else:
+            namespace = ""
 
-    def start(self):
-        if self.args.name is not None:
-            self.host.bridge.encryptionNamespaceGet(self.args.name,
-                callback=self.encryptionNamespaceGetCb,
-                errback=partial(self.errback,
-                                msg=_("Can't get encryption namespace: {}"),
-                                exit_code=C.EXIT_BRIDGE_ERRBACK,
-                                ))
-        elif self.args.namespace is not None:
-            self.encryptionNamespaceGetCb(self.args.namespace)
-        else:
-            self.encryptionNamespaceGetCb("")
+        jids = await self.host.check_jids([self.args.jid])
+        jid = jids[0]
+
+        try:
+            await self.host.bridge.messageEncryptionStart(
+                jid, namespace, not self.args.encrypt_noreplace,
+                self.profile)
+        except Exception as e:
+            self.disp(f"can't get encryption namespace: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        self.host.quit()
 
 
 class EncryptionStop(base.CommandBase):
@@ -160,7 +145,6 @@
         super(EncryptionStop, self).__init__(
             host, "stop",
             help=_("stop encrypted session with an entity"))
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -168,18 +152,16 @@
             help=_("jid of the entity to stop encrypted session with")
         )
 
-    def start(self):
-        jids = self.host.check_jids([self.args.jid])
+    async def start(self):
+        jids = await self.host.check_jids([self.args.jid])
         jid = jids[0]
-        self.host.bridge.messageEncryptionStop(
-            jid, self.profile,
-            callback=self.host.quit,
-            errback=partial(
-                self.errback,
-                msg=_("can't end encrypted session: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.messageEncryptionStop(jid, self.profile)
+        except Exception as e:
+            self.disp(f"can't end encrypted session: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        self.host.quit()
 
 
 class TrustUI(base.CommandBase):
@@ -188,7 +170,6 @@
         super(TrustUI, self).__init__(
             host, "ui",
             help=_("get UI to manage trust"))
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -202,37 +183,33 @@
             "-N", "--namespace",
             help=_("algorithm namespace (DEFAULT: current algorithm)"))
 
-    def encryptionTrustUIGetCb(self, xmlui_raw):
-        xmlui = xmlui_manager.create(self.host, xmlui_raw)
-        xmlui.show()
-        xmlui.submitForm()
+    async def start(self):
+        if self.args.name is not None:
+            try:
+                namespace = await self.host.bridge.encryptionNamespaceGet(self.args.name)
+            except Exception as e:
+                self.disp(f"can't get encryption namespace: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        elif self.args.namespace is not None:
+            namespace = self.args.namespace
+        else:
+            namespace = ""
 
-    def encryptionNamespaceGetCb(self, namespace):
-        jids = self.host.check_jids([self.args.jid])
+        jids = await self.host.check_jids([self.args.jid])
         jid = jids[0]
-        self.host.bridge.encryptionTrustUIGet(
-            jid, namespace, self.profile,
-            callback=self.encryptionTrustUIGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't end encrypted session: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
 
-    def start(self):
-        if self.args.name is not None:
-            self.host.bridge.encryptionNamespaceGet(self.args.name,
-                callback=self.encryptionNamespaceGetCb,
-                errback=partial(self.errback,
-                                msg=_("Can't get encryption namespace: {}"),
-                                exit_code=C.EXIT_BRIDGE_ERRBACK,
-                                ))
-        elif self.args.namespace is not None:
-            self.encryptionNamespaceGetCb(self.args.namespace)
-        else:
-            self.encryptionNamespaceGetCb("")
+        try:
+            xmlui_raw = await self.host.bridge.encryptionTrustUIGet(
+                jid, namespace, self.profile)
+        except Exception as e:
+            self.disp(f"can't get encryption session trust UI: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
+        xmlui = xmlui_manager.create(self.host, xmlui_raw)
+        await xmlui.show()
+        if xmlui.type != C.XMLUI_DIALOG:
+            await xmlui.submitForm()
+        self.host.quit()
 
 class EncryptionTrust(base.CommandBase):
     subcommands = (TrustUI,)
--- a/sat_frontends/jp/cmd_event.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_event.py	Wed Sep 25 08:56:41 2019 +0200
@@ -23,7 +23,6 @@
 from sat.tools.common.ansi import ANSI as A
 from sat_frontends.jp.constants import Const as C
 from sat_frontends.jp import common
-from functools import partial
 from dateutil import parser as du_parser
 import calendar
 import time
@@ -47,30 +46,26 @@
             use_verbose=True,
             help=_("get event data"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def eventInviteeGetCb(self, result):
-        event_date, event_data = result
-        event_data["date"] = event_date
-        self.output(event_data)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.eventGet(
-            self.args.service,
-            self.args.node,
-            self.args.item,
-            self.profile,
-            callback=self.eventInviteeGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get event data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            event_tuple = await self.host.bridge.eventGet(
+                self.args.service,
+                self.args.node,
+                self.args.item,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get event data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            event_date, event_data = event_tuple
+            event_data["date"] = event_date
+            await self.output(event_data)
+            self.host.quit()
 
 
 class EventBase(object):
@@ -126,29 +121,25 @@
             help=_("create or replace event"),
         )
         EventBase.__init__(self)
-        self.need_loop = True
 
-    def eventCreateCb(self, node):
-        self.disp(_("Event created successfuly on node {node}").format(node=node))
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         fields = self.parseFields()
         date = self.parseDate()
-        self.host.bridge.eventCreate(
-            date,
-            fields,
-            self.args.service,
-            self.args.node,
-            self.args.id,
-            self.profile,
-            callback=self.eventCreateCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't create event: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            node = await self.host.bridge.eventCreate(
+                date,
+                fields,
+                self.args.service,
+                self.args.node,
+                self.args.id,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't create event: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f"Event created successfuly on node {node}"))
+            self.host.quit()
 
 
 class Modify(EventBase, base.CommandBase):
@@ -161,25 +152,24 @@
             help=_("modify an existing event"),
         )
         EventBase.__init__(self)
-        self.need_loop = True
 
-    def start(self):
+    async def start(self):
         fields = self.parseFields()
         date = 0 if not self.args.date else self.parseDate()
-        self.host.bridge.eventModify(
-            self.args.service,
-            self.args.node,
-            self.args.id,
-            date,
-            fields,
-            self.profile,
-            callback=self.host.quit,
-            errback=partial(
-                self.errback,
-                msg=_("can't update event data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            self.host.bridge.eventModify(
+                self.args.service,
+                self.args.node,
+                self.args.id,
+                date,
+                fields,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't update event data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class InviteeGet(base.CommandBase):
@@ -190,31 +180,30 @@
             "get",
             use_output=C.OUTPUT_DICT,
             use_pubsub=True,
-            pubsub_flags={C.NODE, C.ITEM, C.SINGLE_ITEM},
+            pubsub_flags={C.NODE},
             use_verbose=True,
             help=_("get event attendance"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
-        pass
-
-    def eventInviteeGetCb(self, event_data):
-        self.output(event_data)
-        self.host.quit()
+        self.parser.add_argument(
+            "-j", "--jid", default="", help=_("bare jid of the invitee")
+        )
 
-    def start(self):
-        self.host.bridge.eventInviteeGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.eventInviteeGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get event data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            event_data = await self.host.bridge.eventInviteeGet(
+                self.args.service,
+                self.args.node,
+                self.args.jid,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get event data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(event_data)
+            self.host.quit()
 
 
 class InviteeSet(base.CommandBase):
@@ -227,7 +216,6 @@
             pubsub_flags={C.NODE},
             help=_("set event attendance"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -240,20 +228,20 @@
             help=_("configuration field to set"),
         )
 
-    def start(self):
+    async def start(self):
         fields = dict(self.args.fields) if self.args.fields else {}
-        self.host.bridge.eventInviteeSet(
-            self.args.service,
-            self.args.node,
-            fields,
-            self.profile,
-            callback=self.host.quit,
-            errback=partial(
-                self.errback,
-                msg=_("can't set event data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            self.host.bridge.eventInviteeSet(
+                self.args.service,
+                self.args.node,
+                fields,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set event data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class InviteesList(base.CommandBase):
@@ -270,7 +258,6 @@
             use_verbose=True,
             help=_("get event attendance"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -399,72 +386,68 @@
                 )
             )
 
-    def eventInviteesListCb(self, event_data, prefilled_data):
-        """fill nicknames and keep only requested people
-
-        @param event_data(dict): R.S.V.P. answers
-        @param prefilled_data(dict): prefilled data with all people
-            only filled if --missing is used
-        """
-        if self.args.no_rsvp:
-            for jid_ in event_data:
-                # if there is a jid in event_data
-                # it must be there in prefilled_data too
-                # so no need to check for KeyError
-                del prefilled_data[jid_]
-        else:
-            # we replace empty dicts for existing people with R.S.V.P. data
-            prefilled_data.update(event_data)
-
-        # we get nicknames for everybody, make it easier for organisers
-        for jid_, data in prefilled_data.items():
-            id_data = self.host.bridge.identityGet(jid_, self.profile)
-            data["nick"] = id_data.get("nick", "")
-
-        self.output(prefilled_data)
-        self.host.quit()
-
-    def getList(self, prefilled_data={}):
-        self.host.bridge.eventInviteesList(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=partial(self.eventInviteesListCb, prefilled_data=prefilled_data),
-            errback=partial(
-                self.errback,
-                msg=_("can't get event data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
-    def psNodeAffiliationsGetCb(self, affiliations):
-        # we fill all affiliations with empty data
-        # answered one will be filled in eventInviteesListCb
-        # we only consider people with "publisher" affiliation as invited, creators are not, and members can just observe
-        prefilled = {
-            jid_: {}
-            for jid_, affiliation in affiliations.items()
-            if affiliation in ("publisher",)
-        }
-        self.getList(prefilled)
-
-    def start(self):
+    async def start(self):
         if self.args.no_rsvp and not self.args.missing:
             self.parser.error(_("you need to use --missing if you use --no-rsvp"))
-        if self.args.missing:
-            self.host.bridge.psNodeAffiliationsGet(
+        if not self.args.missing:
+            prefilled = {}
+        else:
+            # we get prefilled data with all people
+            try:
+                affiliations = await self.host.bridge.psNodeAffiliationsGet(
+                    self.args.service,
+                    self.args.node,
+                    self.profile,
+                )
+            except Exception as e:
+                self.disp(f"can't get node affiliations: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+            else:
+                # we fill all affiliations with empty data, answered one will be filled
+                # below. We only consider people with "publisher" affiliation as invited,
+                # creators are not, and members can just observe
+                prefilled = {
+                    jid_: {}
+                    for jid_, affiliation in affiliations.items()
+                    if affiliation in ("publisher",)
+                }
+
+        try:
+            event_data = await self.host.bridge.eventInviteesList(
                 self.args.service,
                 self.args.node,
                 self.profile,
-                callback=self.psNodeAffiliationsGetCb,
-                errback=partial(
-                    self.errback,
-                    msg=_("can't get event data: {}"),
-                    exit_code=C.EXIT_BRIDGE_ERRBACK,
-                ),
             )
+        except Exception as e:
+            self.disp(f"can't get event data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        # we fill nicknames and keep only requested people
+
+        if self.args.no_rsvp:
+            for jid_ in event_data:
+                # if there is a jid in event_data it must be there in prefilled too
+                # otherwie somebody is not on the invitees list
+                try:
+                    del prefilled[jid_]
+                except KeyError:
+                    self.disp(A.color(
+                        C.A_WARNING,
+                        f"We got a RSVP from somebody who was not in invitees "
+                        f"list: {jid_}"
+                        ),
+                        error=True)
         else:
-            self.getList()
+            # we replace empty dicts for existing people with R.S.V.P. data
+            prefilled.update(event_data)
+
+        # we get nicknames for everybody, make it easier for organisers
+        for jid_, data in prefilled.items():
+            id_data = await self.host.bridge.identityGet(jid_, self.profile)
+            data["nick"] = id_data.get("nick", "")
+
+        await self.output(prefilled)
+        self.host.quit()
 
 
 class InviteeInvite(base.CommandBase):
@@ -477,7 +460,6 @@
             pubsub_flags={C.NODE, C.SINGLE_ITEM},
             help=_("invite someone to the event through email"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -524,30 +506,30 @@
             help="body of the invitation email (default: generic body)",
         )
 
-    def start(self):
+    async def start(self):
         email = self.args.email[0] if self.args.email else None
         emails_extra = self.args.email[1:]
 
-        self.host.bridge.eventInviteByEmail(
-            self.args.service,
-            self.args.node,
-            self.args.item,
-            email,
-            emails_extra,
-            self.args.name,
-            self.args.host_name,
-            self.args.lang,
-            self.args.url_template,
-            self.args.subject,
-            self.args.body,
-            self.args.profile,
-            callback=self.host.quit,
-            errback=partial(
-                self.errback,
-                msg=_("can't create invitation: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.eventInviteByEmail(
+                self.args.service,
+                self.args.node,
+                self.args.item,
+                email,
+                emails_extra,
+                self.args.name,
+                self.args.host_name,
+                self.args.lang,
+                self.args.url_template,
+                self.args.subject,
+                self.args.body,
+                self.args.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't create invitation: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class Invitee(base.CommandBase):
--- a/sat_frontends/jp/cmd_file.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_file.py	Wed Sep 25 08:56:41 2019 +0200
@@ -31,7 +31,6 @@
 from sat.tools.common.ansi import ANSI as A
 import tempfile
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
-from functools import partial
 import json
 
 __commands__ = ["File"]
@@ -46,7 +45,6 @@
             use_verbose=True,
             help=_("send a file to a contact"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -75,23 +73,19 @@
             help=("name to use (DEFAULT: use source file name)"),
         )
 
-    def start(self):
-        """Send files to jabber contact"""
-        self.send_files()
-
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("File copy started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("File sent successfully"), 2)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, error_msg):
         if error_msg == C.PROGRESS_ERROR_DECLINED:
             self.disp(_("The file has been refused by your contact"))
         else:
             self.disp(_("Error while sending file: {}").format(error_msg), error=True)
 
-    def gotId(self, data, file_):
+    async def gotId(self, data, file_):
         """Called when a progress id has been received
 
         @param pid(unicode): progress id
@@ -100,7 +94,7 @@
         # FIXME: this show progress only for last progress_id
         self.disp(_("File request sent to {jid}".format(jid=self.full_dest_jid)), 1)
         try:
-            self.progress_id = data["progress"]
+            await self.set_progress_id(data["progress"])
         except KeyError:
             # TODO: if 'xmlui' key is present, manage xmlui message display
             self.disp(
@@ -108,27 +102,18 @@
             )
             self.host.quit(2)
 
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to send a file: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def send_files(self):
+    async def start(self):
         for file_ in self.args.files:
             if not os.path.exists(file_):
-                self.disp(_("file [{}] doesn't exist !").format(file_), error=True)
-                self.host.quit(1)
+                self.disp(_(f"file {file_!r} doesn't exist!"), error=True)
+                self.host.quit(C.EXIT_BAD_ARG)
             if not self.args.bz2 and os.path.isdir(file_):
                 self.disp(
-                    _(
-                        "[{}] is a dir ! Please send files inside or use compression"
-                    ).format(file_)
+                    _(f"{file_!r} is a dir! Please send files inside or use compression")
                 )
-                self.host.quit(1)
+                self.host.quit(C.EXIT_BAD_ARG)
 
-        self.full_dest_jid = self.host.get_full_jid(self.args.jid)
+        self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
         extra = {}
         if self.args.path:
             extra["path"] = self.args.path
@@ -152,29 +137,37 @@
                 bz2.close()
                 self.disp(_("Done !"), 1)
 
-                self.host.bridge.fileSend(
-                    self.full_dest_jid,
-                    buf.name,
-                    self.args.name or archive_name,
-                    "",
-                    extra,
-                    self.profile,
-                    callback=lambda pid, file_=buf.name: self.gotId(pid, file_),
-                    errback=self.error,
-                )
+                try:
+                    send_data = await self.host.bridge.fileSend(
+                        self.full_dest_jid,
+                        buf.name,
+                        self.args.name or archive_name,
+                        "",
+                        extra,
+                        self.profile,
+                    )
+                except Exception as e:
+                    self.disp(f"can't send file: {e}", error=True)
+                    self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+                else:
+                    await self.gotId(send_data, file_)
         else:
             for file_ in self.args.files:
                 path = os.path.abspath(file_)
-                self.host.bridge.fileSend(
-                    self.full_dest_jid,
-                    path,
-                    self.args.name,
-                    "",
-                    extra,
-                    self.profile,
-                    callback=lambda pid, file_=file_: self.gotId(pid, file_),
-                    errback=self.error,
-                )
+                try:
+                    send_data = await self.host.bridge.fileSend(
+                        self.full_dest_jid,
+                        path,
+                        self.args.name,
+                        "",
+                        extra,
+                        self.profile,
+                    )
+                except Exception as e:
+                    self.disp(f"can't send file {file_!r}: {e}", error=True)
+                    self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+                else:
+                    await self.gotId(send_data, file_)
 
 
 class Request(base.CommandBase):
@@ -186,7 +179,6 @@
             use_verbose=True,
             help=_("request a file from a contact"),
         )
-        self.need_loop = True
 
     @property
     def filename(self):
@@ -200,7 +192,8 @@
             "-D",
             "--dest",
             help=_(
-                "destination path where the file will be saved (default: [current_dir]/[name|hash])"
+                "destination path where the file will be saved (default: "
+                "[current_dir]/[name|hash])"
             ),
         )
         self.parser.add_argument(
@@ -238,36 +231,21 @@
             help=_("overwrite existing file without confirmation"),
         )
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("File copy started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("File received successfully"), 2)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, error_msg):
         if error_msg == C.PROGRESS_ERROR_DECLINED:
             self.disp(_("The file request has been refused"))
         else:
             self.disp(_("Error while requesting file: {}").format(error_msg), error=True)
 
-    def gotId(self, progress_id):
-        """Called when a progress id has been received
-
-        @param progress_id(unicode): progress id
-        """
-        self.progress_id = progress_id
-
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to send a file: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def start(self):
+    async def start(self):
         if not self.args.name and not self.args.hash:
             self.parser.error(_("at least one of --name or --hash must be provided"))
-        #  extra = dict(self.args.extra)
         if self.args.dest:
             path = os.path.abspath(os.path.expanduser(self.args.dest))
             if os.path.isdir(path):
@@ -276,35 +254,30 @@
             path = os.path.abspath(self.filename)
 
         if os.path.exists(path) and not self.args.force:
-            message = _("File {path} already exists! Do you want to overwrite?").format(
-                path=path
-            )
-            confirm = input("{} (y/N) ".format(message).encode("utf-8"))
-            if confirm not in ("y", "Y"):
-                self.disp(_("file request cancelled"))
-                self.host.quit(2)
+            message = _(f"File {path} already exists! Do you want to overwrite?")
+            await self.host.confirmOrQuit(message, _("file request cancelled"))
 
-        self.full_dest_jid = self.host.get_full_jid(self.args.jid)
+        self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
         extra = {}
         if self.args.path:
             extra["path"] = self.args.path
         if self.args.namespace:
             extra["namespace"] = self.args.namespace
-        self.host.bridge.fileJingleRequest(
-            self.full_dest_jid,
-            path,
-            self.args.name,
-            self.args.hash,
-            self.args.hash_algo if self.args.hash else "",
-            extra,
-            self.profile,
-            callback=self.gotId,
-            errback=partial(
-                self.errback,
-                msg=_("can't request file: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            progress_id = await self.host.bridge.fileJingleRequest(
+                self.full_dest_jid,
+                path,
+                self.args.name,
+                self.args.hash,
+                self.args.hash_algo if self.args.hash else "",
+                extra,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(msg=_(f"can't request file: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.set_progress_id(progress_id)
 
 
 class Receive(base.CommandAnswering):
@@ -322,89 +295,6 @@
             C.META_TYPE_OVERWRITE: self.onOverwriteAction,
         }
 
-    def onProgressStarted(self, metadata):
-        self.disp(_("File copy started"), 2)
-
-    def onProgressFinished(self, metadata):
-        self.disp(_("File received successfully"), 2)
-        if metadata.get("hash_verified", False):
-            try:
-                self.disp(
-                    _("hash checked: {algo}:{checksum}").format(
-                        algo=metadata["hash_algo"], checksum=metadata["hash"]
-                    ),
-                    1,
-                )
-            except KeyError:
-                self.disp(_("hash is checked but hash value is missing", 1), error=True)
-        else:
-            self.disp(_("hash can't be verified"), 1)
-
-    def onProgressError(self, error_msg):
-        self.disp(_("Error while receiving file: {}").format(error_msg), error=True)
-
-    def getXmluiId(self, action_data):
-        # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
-        #        should be available in the futur
-        # TODO: XMLUI module
-        try:
-            xml_ui = action_data["xmlui"]
-        except KeyError:
-            self.disp(_("Action has no XMLUI"), 1)
-        else:
-            ui = ET.fromstring(xml_ui.encode("utf-8"))
-            xmlui_id = ui.get("submit")
-            if not xmlui_id:
-                self.disp(_("Invalid XMLUI received"), error=True)
-            return xmlui_id
-
-    def onFileAction(self, action_data, action_id, security_limit, profile):
-        xmlui_id = self.getXmluiId(action_data)
-        if xmlui_id is None:
-            return self.host.quitFromSignal(1)
-        try:
-            from_jid = jid.JID(action_data["meta_from_jid"])
-        except KeyError:
-            self.disp(_("Ignoring action without from_jid data"), 1)
-            return
-        try:
-            progress_id = action_data["meta_progress_id"]
-        except KeyError:
-            self.disp(_("ignoring action without progress id"), 1)
-            return
-
-        if not self.bare_jids or from_jid.bare in self.bare_jids:
-            if self._overwrite_refused:
-                self.disp(_("File refused because overwrite is needed"), error=True)
-                self.host.bridge.launchAction(
-                    xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile
-                )
-                return self.host.quitFromSignal(2)
-            self.progress_id = progress_id
-            xmlui_data = {"path": self.path}
-            self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
-
-    def onOverwriteAction(self, action_data, action_id, security_limit, profile):
-        xmlui_id = self.getXmluiId(action_data)
-        if xmlui_id is None:
-            return self.host.quitFromSignal(1)
-        try:
-            progress_id = action_data["meta_progress_id"]
-        except KeyError:
-            self.disp(_("ignoring action without progress id"), 1)
-            return
-        self.disp(_("Overwriting needed"), 1)
-
-        if progress_id == self.progress_id:
-            if self.args.force:
-                self.disp(_("Overwrite accepted"), 2)
-            else:
-                self.disp(_("Refused to overwrite"), 2)
-                self._overwrite_refused = True
-
-            xmlui_data = {"answer": C.boolConst(self.args.force)}
-            self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
-
     def add_parser_options(self):
         self.parser.add_argument(
             "jids",
@@ -432,15 +322,95 @@
             help=_("destination path (default: working directory)"),
         )
 
-    def start(self):
+    async def onProgressStarted(self, metadata):
+        self.disp(_("File copy started"), 2)
+
+    async def onProgressFinished(self, metadata):
+        self.disp(_("File received successfully"), 2)
+        if metadata.get("hash_verified", False):
+            try:
+                self.disp(_(
+                    f"hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1)
+            except KeyError:
+                self.disp(_("hash is checked but hash value is missing", 1), error=True)
+        else:
+            self.disp(_("hash can't be verified"), 1)
+
+    async def onProgressError(self, e):
+        self.disp(_(f"Error while receiving file: {e}"), error=True)
+
+    def getXmluiId(self, action_data):
+        # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
+        #        should be available in the futur
+        # TODO: XMLUI module
+        try:
+            xml_ui = action_data["xmlui"]
+        except KeyError:
+            self.disp(_("Action has no XMLUI"), 1)
+        else:
+            ui = ET.fromstring(xml_ui.encode("utf-8"))
+            xmlui_id = ui.get("submit")
+            if not xmlui_id:
+                self.disp(_("Invalid XMLUI received"), error=True)
+            return xmlui_id
+
+    async def onFileAction(self, action_data, action_id, security_limit, profile):
+        xmlui_id = self.getXmluiId(action_data)
+        if xmlui_id is None:
+            return self.host.quitFromSignal(1)
+        try:
+            from_jid = jid.JID(action_data["meta_from_jid"])
+        except KeyError:
+            self.disp(_("Ignoring action without from_jid data"), 1)
+            return
+        try:
+            progress_id = action_data["meta_progress_id"]
+        except KeyError:
+            self.disp(_("ignoring action without progress id"), 1)
+            return
+
+        if not self.bare_jids or from_jid.bare in self.bare_jids:
+            if self._overwrite_refused:
+                self.disp(_("File refused because overwrite is needed"), error=True)
+                await self.host.bridge.launchAction(
+                    xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile
+                )
+                return self.host.quitFromSignal(2)
+            await self.set_progress_id(progress_id)
+            xmlui_data = {"path": self.path}
+            await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
+
+    async def onOverwriteAction(self, action_data, action_id, security_limit, profile):
+        xmlui_id = self.getXmluiId(action_data)
+        if xmlui_id is None:
+            return self.host.quitFromSignal(1)
+        try:
+            progress_id = action_data["meta_progress_id"]
+        except KeyError:
+            self.disp(_("ignoring action without progress id"), 1)
+            return
+        self.disp(_("Overwriting needed"), 1)
+
+        if progress_id == self.progress_id:
+            if self.args.force:
+                self.disp(_("Overwrite accepted"), 2)
+            else:
+                self.disp(_("Refused to overwrite"), 2)
+                self._overwrite_refused = True
+
+            xmlui_data = {"answer": C.boolConst(self.args.force)}
+            await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
+
+    async def start(self):
         self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids]
         self.path = os.path.abspath(self.args.path)
         if not os.path.isdir(self.path):
             self.disp(_("Given path is not a directory !", error=True))
-            self.host.quit(2)
+            self.host.quit(C.EXIT_BAD_ARG)
         if self.args.multiple:
             self.host.quit_on_progress_end = False
         self.disp(_("waiting for incoming file request"), 2)
+        await self.start_answering()
 
 
 class Upload(base.CommandBase):
@@ -448,7 +418,6 @@
         super(Upload, self).__init__(
             host, "upload", use_progress=True, use_verbose=True, help=_("upload a file")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument("file", type=str, help=_("file to upload"))
@@ -463,10 +432,10 @@
             help=_("ignore invalide TLS certificate"),
         )
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("File upload started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("File uploaded successfully"), 2)
         try:
             url = metadata["url"]
@@ -477,55 +446,54 @@
             # XXX: url is display alone on a line to make parsing easier
             self.disp(url)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, error_msg):
         self.disp(_("Error while uploading file: {}").format(error_msg), error=True)
 
-    def gotId(self, data, file_):
+    async def gotId(self, data, file_):
         """Called when a progress id has been received
 
         @param pid(unicode): progress id
         @param file_(str): file path
         """
         try:
-            self.progress_id = data["progress"]
+            await self.set_progress_id(data["progress"])
         except KeyError:
             # TODO: if 'xmlui' key is present, manage xmlui message display
             self.disp(_("Can't upload file"), error=True)
-            self.host.quit(2)
+            self.host.quit(C.EXIT_ERROR)
 
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to upload a file: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def start(self):
+    async def start(self):
         file_ = self.args.file
         if not os.path.exists(file_):
-            self.disp(_("file [{}] doesn't exist !").format(file_), error=True)
-            self.host.quit(1)
+            self.disp(_(f"file {file_!r} doesn't exist !"), error=True)
+            self.host.quit(C.EXIT_BAD_ARG)
         if os.path.isdir(file_):
-            self.disp(_("[{}] is a dir! Can't upload a dir").format(file_))
-            self.host.quit(1)
+            self.disp(_(f"{file_!r} is a dir! Can't upload a dir"))
+            self.host.quit(C.EXIT_BAD_ARG)
 
-        self.full_dest_jid = (
-            self.host.get_full_jid(self.args.jid) if self.args.jid is not None else ""
-        )
+        if self.args.jid is None:
+            self.full_dest_jid = None
+        else:
+            self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
+
         options = {}
         if self.args.ignore_tls_errors:
             options["ignore_tls_errors"] = C.BOOL_TRUE
 
         path = os.path.abspath(file_)
-        self.host.bridge.fileUpload(
-            path,
-            "",
-            self.full_dest_jid,
-            options,
-            self.profile,
-            callback=lambda pid, file_=file_: self.gotId(pid, file_),
-            errback=self.error,
-        )
+        try:
+            upload_data = await self.host.bridge.fileUpload(
+                path,
+                "",
+                self.full_dest_jid,
+                options,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't while trying to upload a file: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.gotId(upload_data, file_)
 
 
 class ShareList(base.CommandBase):
@@ -539,7 +507,6 @@
             help=_("retrieve files shared by an entity"),
             use_verbose=True,
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -555,12 +522,6 @@
             help=_("jid of sharing entity (nothing to check our own jid)"),
         )
 
-    def file_gen(self, files_data):
-        for file_data in files_data:
-            yield file_data["name"]
-            yield file_data.get("size", "")
-            yield file_data.get("hash", "")
-
     def _name_filter(self, name, row):
         if row.type == C.FILE_TYPE_DIRECTORY:
             return A.color(C.A_DIRECTORY, name)
@@ -588,38 +549,37 @@
         files_data.sort(key=lambda d: d["name"].lower())
         show_header = False
         if self.verbosity == 0:
-            headers = ("name", "type")
+            keys = headers = ("name", "type")
         elif self.verbosity == 1:
-            headers = ("name", "type", "size")
+            keys = headers = ("name", "type", "size")
         elif self.verbosity > 1:
             show_header = True
+            keys = ("name", "type", "size", "file_hash")
             headers = ("name", "type", "size", "hash")
         table = common.Table.fromDict(
             self.host,
             files_data,
-            headers,
+            keys=keys,
+            headers=headers,
             filters={"name": self._name_filter, "size": self._size_filter},
-            defaults={"size": "", "hash": ""},
+            defaults={"size": "", "file_hash": ""},
         )
         table.display_blank(show_header=show_header, hide_cols=["type"])
 
-    def _FISListCb(self, files_data):
-        self.output(files_data)
-        self.host.quit()
+    async def start(self):
+        try:
+            files_data = await self.host.bridge.FISList(
+                self.args.jid,
+                self.args.path,
+                {},
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't retrieve shared files: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def start(self):
-        self.host.bridge.FISList(
-            self.args.jid,
-            self.args.path,
-            {},
-            self.profile,
-            callback=self._FISListCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't retrieve shared files: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        await self.output(files_data)
+        self.host.quit()
 
 
 class SharePath(base.CommandBase):
@@ -627,7 +587,6 @@
         super(SharePath, self).__init__(
             host, "path", help=_("share a file or directory"), use_verbose=True
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -640,6 +599,7 @@
         perm_group.add_argument(
             "-j",
             "--jid",
+            metavar="JID",
             action="append",
             dest="jids",
             default=[],
@@ -649,7 +609,8 @@
             "--public",
             action="store_true",
             help=_(
-                "share publicly the file(s) (/!\\ *everybody* will be able to access them)"
+                r"share publicly the file(s) (/!\ *everybody* will be able to access "
+                r"them)"
             ),
         )
         self.parser.add_argument(
@@ -657,13 +618,7 @@
             help=_("path to a file or directory to share"),
         )
 
-    def _FISSharePathCb(self, name):
-        self.disp(
-            _('{path} shared under the name "{name}"').format(path=self.path, name=name)
-        )
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         self.path = os.path.abspath(self.args.path)
         if self.args.public:
             access = {"read": {"type": "public"}}
@@ -673,18 +628,19 @@
                 access = {"read": {"type": "whitelist", "jids": jids}}
             else:
                 access = {}
-        self.host.bridge.FISSharePath(
-            self.args.name,
-            self.path,
-            json.dumps(access, ensure_ascii=False),
-            self.profile,
-            callback=self._FISSharePathCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't share path: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            name = await self.host.bridge.FISSharePath(
+                self.args.name,
+                self.path,
+                json.dumps(access, ensure_ascii=False),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't share path: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f'{self.path} shared under the name "{name}"'))
+            self.host.quit()
 
 
 class ShareInvite(base.CommandBase):
@@ -692,7 +648,6 @@
         super(ShareInvite, self).__init__(
             host, "invite", help=_("send invitation for a shared repository")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -733,13 +688,7 @@
             help=_("jid of the person to invite"),
         )
 
-    def _FISInviteCb(self):
-        self.disp(
-            _('invitation sent to {entity}').format(entity=self.args.jid)
-        )
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         self.path = os.path.normpath(self.args.path) if self.args.path else ""
         extra = {}
         if self.args.thumbnail is not None:
@@ -747,22 +696,25 @@
                 self.parser.error(_("only http(s) links are allowed with --thumbnail"))
             else:
                 extra['thumb_url'] = self.args.thumbnail
-        self.host.bridge.FISInvite(
-            self.args.jid,
-            self.args.service,
-            self.args.type,
-            self.args.namespace,
-            self.path,
-            self.args.name,
-            data_format.serialise(extra),
-            self.profile,
-            callback=self._FISInviteCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't send invitation: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.FISInvite(
+                self.args.jid,
+                self.args.service,
+                self.args.type,
+                self.args.namespace,
+                self.path,
+                self.args.name,
+                data_format.serialise(extra),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't send invitation: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(
+                _(f'invitation sent to {self.args.jid}')
+            )
+            self.host.quit()
 
 
 class Share(base.CommandBase):
--- a/sat_frontends/jp/cmd_forums.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_forums.py	Wed Sep 25 08:56:41 2019 +0200
@@ -23,7 +23,6 @@
 from sat_frontends.jp.constants import Const as C
 from sat_frontends.jp import common
 from sat.tools.common.ansi import ANSI as A
-from functools import partial
 import codecs
 import json
 
@@ -46,7 +45,6 @@
             help=_("edit forums"),
         )
         common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR)
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -60,26 +58,37 @@
         """return suffix used for content file"""
         return "json"
 
-    def forumsSetCb(self):
-        self.disp(_("forums have been edited"), 1)
-        self.host.quit()
+    async def publish(self, forums_raw):
+        try:
+            await self.host.bridge.forumsSet(
+                forums_raw,
+                self.args.service,
+                self.args.node,
+                self.args.key,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set forums: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("forums have been edited"), 1)
+            self.host.quit()
 
-    def publish(self, forums_raw):
-        self.host.bridge.forumsSet(
-            forums_raw,
-            self.args.service,
-            self.args.node,
-            self.args.key,
-            self.profile,
-            callback=self.forumsSetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't set forums: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            forums_json = await self.host.bridge.forumsGet(
+                self.args.service,
+                self.args.node,
+                self.args.key,
+                self.profile,
+            )
+        except Exception as e:
+            if e.classname == "NotFound":
+                forums_json = ""
+            else:
+                self.disp(f"can't get node configuration: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def forumsGetCb(self, forums_json):
         content_file_obj, content_file_path = self.getTmpFile()
         forums_json = forums_json.strip()
         if forums_json:
@@ -89,28 +98,7 @@
             f = codecs.getwriter("utf-8")(content_file_obj)
             json.dump(forums, f, ensure_ascii=False, indent=4)
             content_file_obj.seek(0)
-        self.runEditor("forums_editor_args", content_file_path, content_file_obj)
-
-    def forumsGetEb(self, failure_):
-        # FIXME: error handling with bridge is broken, need to be properly fixed
-        if failure_.condition == "item-not-found":
-            self.forumsGetCb("")
-        else:
-            self.errback(
-                failure_,
-                msg=_("can't get forums structure: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            )
-
-    def start(self):
-        self.host.bridge.forumsGet(
-            self.args.service,
-            self.args.node,
-            self.args.key,
-            self.profile,
-            callback=self.forumsGetCb,
-            errback=self.forumsGetEb,
-        )
+        await self.runEditor("forums_editor_args", content_file_path, content_file_obj)
 
 
 class Get(base.CommandBase):
@@ -126,7 +114,6 @@
             use_verbose=True,
             help=_("get forums structure"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -165,27 +152,24 @@
                         A.color(level * 4 * " ", head_color, key, A.RESET, ": ", value)
                     )
 
-    def forumsGetCb(self, forums_raw):
-        if not forums_raw:
-            self.disp(_("no schema found"), 1)
-            self.host.quit(1)
-        forums = json.loads(forums_raw)
-        self.output(forums)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.forumsGet(
-            self.args.service,
-            self.args.node,
-            self.args.key,
-            self.profile,
-            callback=self.forumsGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get forums: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            forums_raw = await self.host.bridge.forumsGet(
+                self.args.service,
+                self.args.node,
+                self.args.key,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get forums: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            if not forums_raw:
+                self.disp(_("no schema found"), 1)
+                self.host.quit(1)
+            forums = json.loads(forums_raw)
+            await self.output(forums)
+            self.host.quit()
 
 
 class Forums(base.CommandBase):
--- a/sat_frontends/jp/cmd_identity.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_identity.py	Wed Sep 25 08:56:41 2019 +0200
@@ -21,12 +21,9 @@
 from . import base
 from sat.core.i18n import _
 from sat_frontends.jp.constants import Const as C
-from functools import partial
 
 __commands__ = ["Identity"]
 
-# TODO: move date parsing to base, it may be useful for other commands
-
 
 class Get(base.CommandBase):
     def __init__(self, host):
@@ -38,29 +35,25 @@
             use_verbose=True,
             help=_("get identity data"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
             "jid", help=_("entity to check")
         )
 
-    def identityGetCb(self, data):
-        self.output(data)
-        self.host.quit()
-
-    def start(self):
-        jid_ = self.host.check_jids([self.args.jid])[0]
-        self.host.bridge.identityGet(
-            jid_,
-            self.profile,
-            callback=self.identityGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get identity data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        jid_ = (await self.host.check_jids([self.args.jid]))[0]
+        try:
+            data = await self.host.bridge.identityGet(
+                jid_,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get identity data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(data)
+            self.host.quit()
 
 
 class Set(base.CommandBase):
@@ -78,20 +71,19 @@
             required=True,
             help=_("identity field(s) to set"),
         )
-        self.need_loop = True
 
-    def start(self):
+    async def start(self):
         fields = dict(self.args.fields)
-        self.host.bridge.identitySet(
-            fields,
-            self.profile,
-            callback=self.host.quit,
-            errback=partial(
-                self.errback,
-                msg=_("can't set identity data data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            self.host.bridge.identitySet(
+                fields,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set identity data: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class Identity(base.CommandBase):
--- a/sat_frontends/jp/cmd_info.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_info.py	Wed Sep 25 08:56:41 2019 +0200
@@ -31,53 +31,19 @@
 
     def __init__(self, host):
         extra_outputs = {'default': self.default_output}
-        super(Disco, self).__init__(host, 'disco', use_output='complex', extra_outputs=extra_outputs, help=_('service discovery'))
-        self.need_loop=True
+        super(Disco, self).__init__(
+            host, 'disco', use_output='complex', extra_outputs=extra_outputs,
+            help=_('service discovery'))
 
     def add_parser_options(self):
         self.parser.add_argument("jid", help=_("entity to discover"))
-        self.parser.add_argument("-t", "--type", type=str, choices=('infos', 'items', 'both'), default='both', help=_("type of data to discover"))
+        self.parser.add_argument(
+            "-t", "--type", type=str, choices=('infos', 'items', 'both'), default='both',
+            help=_("type of data to discover"))
         self.parser.add_argument("-n", "--node", default='', help=_("node to use"))
-        self.parser.add_argument("-C", "--no-cache", dest='use_cache', action="store_false", help=_("ignore cache"))
-
-    def start(self):
-        self.get_infos = self.args.type in ('infos', 'both')
-        self.get_items = self.args.type in ('items', 'both')
-        jids = self.host.check_jids([self.args.jid])
-        jid = jids[0]
-        if not self.get_infos:
-            self.gotInfos(None, jid)
-        else:
-            self.host.bridge.discoInfos(jid, node=self.args.node, use_cache=self.args.use_cache, profile_key=self.host.profile, callback=lambda infos: self.gotInfos(infos, jid), errback=self.error)
-
-    def error(self, failure):
-        print((_("Error while doing discovery [%s]") % failure))
-        self.host.quit(1)
-
-    def gotInfos(self, infos, jid):
-        if not self.get_items:
-            self.gotItems(infos, None)
-        else:
-            self.host.bridge.discoItems(jid, node=self.args.node, use_cache=self.args.use_cache, profile_key=self.host.profile, callback=lambda items: self.gotItems(infos, items), errback=self.error)
-
-    def gotItems(self, infos, items):
-        data = {}
-
-        if self.get_infos:
-            features, identities, extensions = infos
-            features.sort()
-            identities.sort(key=lambda identity: identity[2])
-            data.update({
-                'features': features,
-                'identities': identities,
-                'extensions': extensions})
-
-        if self.get_items:
-            items.sort(key=lambda item: item[2])
-            data['items'] = items
-
-        self.output(data)
-        self.host.quit()
+        self.parser.add_argument(
+            "-C", "--no-cache", dest='use_cache', action="store_false",
+            help=_("ignore cache"))
 
     def default_output(self, data):
         features = data.get('features', [])
@@ -115,15 +81,18 @@
                     for value in values:
                         field_lines.append(A.color('\t  - ', A.BOLD, value))
                 fields.append('\n'.join(field_lines))
-            extensions_tpl.append('{type_}\n{fields}'.format(type_=type_,
-                                                              fields='\n\n'.join(fields)))
+            extensions_tpl.append('{type_}\n{fields}'.format(
+                type_=type_,
+                fields='\n\n'.join(fields)))
 
-        items_table = common.Table(self.host,
-                                   items,
-                                   headers=(_('entity'),
-                                            _('node'),
-                                            _('name')),
-                                   use_buffer=True)
+        items_table = common.Table(
+            self.host,
+            items,
+            headers=(
+                _('entity'),
+                _('node'),
+                _('name')),
+            use_buffer=True)
 
         template = []
         if features:
@@ -135,76 +104,131 @@
         if items:
             template.append(A.color(C.A_HEADER, _("Items")) + "\n\n{items}")
 
-        print("\n\n".join(template).format(features = '\n'.join(features),
-                                            identities = identities_table.display().string,
-                                            extensions = '\n'.join(extensions_tpl),
-                                            items = items_table.display().string,
-                                           ))
+        print("\n\n".join(template).format(
+            features = '\n'.join(features),
+            identities = identities_table.display().string,
+            extensions = '\n'.join(extensions_tpl),
+            items = items_table.display().string,
+            ))
+
+    async def start(self):
+        infos_requested = self.args.type in ('infos', 'both')
+        items_requested = self.args.type in ('items', 'both')
+        jids = await self.host.check_jids([self.args.jid])
+        jid = jids[0]
+
+        # infos
+        if not infos_requested:
+            infos = None
+        else:
+            try:
+                infos = await self.host.bridge.discoInfos(
+                    jid,
+                    node=self.args.node,
+                    use_cache=self.args.use_cache,
+                    profile_key=self.host.profile
+                )
+            except Exception as e:
+                self.disp(_(f"error while doing discovery: {e}"), error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
+        # items
+        if not items_requested:
+            items = None
+        else:
+            try:
+                items = await self.host.bridge.discoItems(
+                    jid,
+                    node=self.args.node,
+                    use_cache=self.args.use_cache,
+                    profile_key=self.host.profile
+                )
+            except Exception as e:
+                self.disp(_(f"error while doing discovery: {e}"), error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        # output
+        data = {}
+
+        if infos_requested:
+            features, identities, extensions = infos
+            features.sort()
+            identities.sort(key=lambda identity: identity[2])
+            data.update({
+                'features': features,
+                'identities': identities,
+                'extensions': extensions})
+
+        if items_requested:
+            items.sort(key=lambda item: item[2])
+            data['items'] = items
+
+        await self.output(data)
+        self.host.quit()
 
 class Version(base.CommandBase):
 
     def __init__(self, host):
         super(Version, self).__init__(host, 'version', help=_('software version'))
-        self.need_loop=True
 
     def add_parser_options(self):
         self.parser.add_argument("jid", type=str, help=_("Entity to request"))
 
-    def start(self):
-        jids = self.host.check_jids([self.args.jid])
+    async def start(self):
+        jids = await self.host.check_jids([self.args.jid])
         jid = jids[0]
-        self.host.bridge.getSoftwareVersion(jid, self.host.profile, callback=self.gotVersion, errback=self.error)
-
-    def error(self, failure):
-        print((_("Error while trying to get version [%s]") % failure))
-        self.host.quit(1)
+        try:
+            data = await self.host.bridge.getSoftwareVersion(jid, self.host.profile)
+        except Exception as e:
+            self.disp(_(f"error while trying to get version: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            infos = []
+            name, version, os = data
+            if name:
+                infos.append(_(f"Software name: {name}"))
+            if version:
+                infos.append(_(f"Software version: {version}"))
+            if os:
+                infos.append(_(f"Operating System: {os}"))
 
-    def gotVersion(self, data):
-        infos = []
-        name, version, os = data
-        if name:
-            infos.append(_("Software name: %s") %  name)
-        if version:
-            infos.append(_("Software version: %s") %  version)
-        if os:
-            infos.append(_("Operating System: %s") %  os)
-
-        print("\n".join(infos))
-        self.host.quit()
+            print("\n".join(infos))
+            self.host.quit()
 
 
 class Session(base.CommandBase):
 
     def __init__(self, host):
         extra_outputs = {'default': self.default_output}
-        super(Session, self).__init__(host, 'session', use_output='dict', extra_outputs=extra_outputs, help=_('running session'))
-        self.need_loop=True
+        super(Session, self).__init__(
+            host, 'session', use_output='dict', extra_outputs=extra_outputs,
+            help=_('running session'))
 
-    def default_output(self, data):
+    def add_parser_options(self):
+        pass
+
+    async def default_output(self, data):
         started = data['started']
         data['started'] = '{short} (UTC, {relative})'.format(
             short=date_utils.date_fmt(started),
             relative=date_utils.date_fmt(started, 'relative'))
-        self.host.output(C.OUTPUT_DICT, 'simple', {}, data)
-
-    def add_parser_options(self):
-        pass
-
-    def start(self):
-        self.host.bridge.sessionInfosGet(self.host.profile, callback=self._sessionInfoGetCb, errback=self._sessionInfoGetEb)
+        await self.host.output(C.OUTPUT_DICT, 'simple', {}, data)
 
-    def _sessionInfoGetCb(self, data):
-        self.output(data)
-        self.host.quit()
-
-    def _sessionInfoGetEb(self, error_data):
-        self.disp(_('Error getting session infos: {}').format(error_data), error=True)
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+    async def start(self):
+        try:
+            data = await self.host.bridge.sessionInfosGet(self.host.profile)
+        except Exception as e:
+            self.disp(_(f'Error getting session infos: {e}'), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(data)
+            self.host.quit()
 
 
 class Info(base.CommandBase):
     subcommands = (Disco, Version, Session)
 
     def __init__(self, host):
-        super(Info, self).__init__(host, 'info', use_profile=False, help=_('Get various pieces of information on entities'))
+        super(Info, self).__init__(
+            host, 'info', use_profile=False,
+            help=_('Get various pieces of information on entities'))
--- a/sat_frontends/jp/cmd_input.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_input.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,14 +18,16 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+import subprocess
+import argparse
+import sys
+import shlex
+import asyncio
 from . import base
 from sat.core.i18n import _
 from sat.core import exceptions
 from sat_frontends.jp.constants import Const as C
 from sat.tools.common.ansi import ANSI as A
-import subprocess
-import argparse
-import sys
 
 __commands__ = ["Input"]
 OPT_STDIN = "stdin"
@@ -105,10 +107,10 @@
             help=_("don't actually run commands but echo what would be launched"),
         )
         self.parser.add_argument(
-            "--log", type=argparse.FileType("wb"), help=_("log stdout to FILE")
+            "--log", type=argparse.FileType("w"), help=_("log stdout to FILE")
         )
         self.parser.add_argument(
-            "--log-err", type=argparse.FileType("wb"), help=_("log stderr to FILE")
+            "--log-err", type=argparse.FileType("w"), help=_("log stderr to FILE")
         )
         self.parser.add_argument("command", nargs=argparse.REMAINDER)
 
@@ -166,15 +168,15 @@
 
         for v in value:
             if arg_type == OPT_STDIN:
-                self._stdin.append(v.encode("utf-8"))
+                self._stdin.append(v)
             elif arg_type == OPT_SHORT:
                 self._opts.append("-{}".format(arg_name))
-                self._opts.append(v.encode("utf-8"))
+                self._opts.append(v)
             elif arg_type == OPT_LONG:
                 self._opts.append("--{}".format(arg_name))
-                self._opts.append(v.encode("utf-8"))
+                self._opts.append(v)
             elif arg_type == OPT_POS:
-                self._pos.append(v.encode("utf-8"))
+                self._pos.append(v)
             elif arg_type == OPT_IGNORE:
                 pass
             else:
@@ -184,7 +186,7 @@
                     ).format(type_=arg_type, name=arg_name)
                 )
 
-    def runCommand(self):
+    async def runCommand(self):
         """run requested command with parsed arguments"""
         if self.args_idx != len(self.args.arguments):
             self.disp(
@@ -200,7 +202,10 @@
         if self.args.debug:
             self.disp(
                 A.color(
-                    C.A_SUBHEADER, _("values: "), A.RESET, ", ".join(self._values_ori)
+                    C.A_SUBHEADER,
+                    _("values: "),
+                    A.RESET,
+                    ", ".join([shlex.quote(a) for a in self._values_ori])
                 ),
                 2,
             )
@@ -209,34 +214,39 @@
                 self.disp(A.color(C.A_SUBHEADER, "--- STDIN ---"))
                 self.disp(stdin)
                 self.disp(A.color(C.A_SUBHEADER, "-------------"))
+
             self.disp(
                 "{indent}{prog} {static} {options} {positionals}".format(
                     indent=4 * " ",
                     prog=sys.argv[0],
                     static=" ".join(self.args.command),
-                    options=" ".join([o for o in self._opts]),
-                    positionals=" ".join([p for p in self._pos]),
+                    options=" ".join(shlex.quote(o) for o in self._opts),
+                    positionals=" ".join(shlex.quote(p) for p in self._pos),
                 )
             )
             self.disp("\n")
         else:
             self.disp(" (" + ", ".join(self._values_ori) + ")", 2, no_lf=True)
             args = [sys.argv[0]] + self.args.command + self._opts + self._pos
-            p = subprocess.Popen(
-                args,
+            p = await asyncio.create_subprocess_exec(
+                *args,
                 stdin=subprocess.PIPE,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
             )
-            (stdout, stderr) = p.communicate(stdin)
+            stdout, stderr = await p.communicate(stdin.encode('utf-8'))
             log = self.args.log
             log_err = self.args.log_err
             log_tpl = "{command}\n{buff}\n\n"
             if log:
-                log.write(log_tpl.format(command=" ".join(args), buff=stdout))
+                log.write(log_tpl.format(
+                    command=" ".join(shlex.quote(a) for a in args),
+                    buff=stdout.decode('utf-8', 'replace')))
             if log_err:
-                log_err.write(log_tpl.format(command=" ".join(args), buff=stderr))
-            ret = p.wait()
+                log_err.write(log_tpl.format(
+                    command=" ".join(shlex.quote(a) for a in args),
+                    buff=stderr.decode('utf-8', 'replace')))
+            ret = p.returncode
             if ret == 0:
                 self.disp(A.color(C.A_SUCCESS, _("OK")))
             else:
@@ -307,21 +317,25 @@
 
         super(Csv, self).filter(filter_type, filter_arg, value)
 
-    def start(self):
+    async def start(self):
         import csv
 
+        if self.args.encoding:
+            sys.stdin.reconfigure(encoding=self.args.encoding, errors="replace")
         reader = csv.reader(sys.stdin)
         for idx, row in enumerate(reader):
             try:
                 if idx < self.args.row:
                     continue
                 for value in row:
-                    self.addValue(value.decode(self.args.encoding))
-                self.runCommand()
+                    self.addValue(value)
+                await self.runCommand()
             except exceptions.CancelError:
                 #  this row has been cancelled, we skip it
                 continue
 
+        self.host.quit()
+
 
 class Input(base.CommandBase):
     subcommands = (Csv,)
--- a/sat_frontends/jp/cmd_invitation.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_invitation.py	Wed Sep 25 08:56:41 2019 +0200
@@ -23,7 +23,6 @@
 from sat_frontends.jp.constants import Const as C
 from sat.tools.common.ansi import ANSI as A
 from sat.tools.common import data_format
-from functools import partial
 
 __commands__ = ["Invitation"]
 
@@ -38,7 +37,6 @@
             use_output=C.OUTPUT_DICT,
             help=_("create and send an invitation"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -115,17 +113,7 @@
             help="profile doing the invitation (default: don't associate profile)",
         )
 
-    def invitationCreateCb(self, invitation_data):
-        self.output(invitation_data)
-        self.host.quit(C.EXIT_OK)
-
-    def invitationCreateEb(self, failure_):
-        self.disp(
-            "can't create invitation: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
+    async def start(self):
         extra = dict(self.args.extra)
         email = self.args.email[0] if self.args.email else None
         emails_extra = self.args.email[1:]
@@ -139,22 +127,27 @@
                     _("you need to specify an email address to send email invitation")
                 )
 
-        self.host.bridge.invitationCreate(
-            email,
-            emails_extra,
-            self.args.jid,
-            self.args.password,
-            self.args.name,
-            self.args.host_name,
-            self.args.lang,
-            self.args.url,
-            self.args.subject,
-            self.args.body,
-            extra,
-            self.args.profile,
-            callback=self.invitationCreateCb,
-            errback=self.invitationCreateEb,
-        )
+        try:
+            invitation_data = await self.host.bridge.invitationCreate(
+                email,
+                emails_extra,
+                self.args.jid,
+                self.args.password,
+                self.args.name,
+                self.args.host_name,
+                self.args.lang,
+                self.args.url,
+                self.args.subject,
+                self.args.body,
+                extra,
+                self.args.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't create invitation: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(invitation_data)
+            self.host.quit(C.EXIT_OK)
 
 
 class Get(base.CommandBase):
@@ -167,7 +160,6 @@
             use_output=C.OUTPUT_DICT,
             help=_("get invitation data"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -180,52 +172,45 @@
             help=_("start profile session and retrieve jid"),
         )
 
-    def output_data(self, data, jid_=None):
+    async def output_data(self, data, jid_=None):
         if jid_ is not None:
             data["jid"] = jid_
-        self.output(data)
+        await self.output(data)
         self.host.quit()
 
-    def invitationGetCb(self, invitation_data):
-        if self.args.with_jid:
+    async def start(self):
+        try:
+            invitation_data = await self.host.bridge.invitationGet(
+                self.args.id,
+            )
+        except Exception as e:
+            self.disp(msg=_(f"can't get invitation data: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        if not self.args.with_jid:
+            await self.output_data(invitation_data)
+        else:
             profile = invitation_data["guest_profile"]
+            try:
+                await self.host.bridge.profileStartSession(
+                    invitation_data["password"],
+                    profile,
+                )
+            except Exception as e:
+                self.disp(msg=_(f"can't start session: {e}"), error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-            def session_started(__):
-                self.host.bridge.asyncGetParamA(
+            try:
+                jid_ = await self.host.bridge.asyncGetParamA(
                     "JabberID",
                     "Connection",
                     profile_key=profile,
-                    callback=lambda jid_: self.output_data(invitation_data, jid_),
-                    errback=partial(
-                        self.errback,
-                        msg=_("can't retrieve jid: {}"),
-                        exit_code=C.EXIT_BRIDGE_ERRBACK,
-                    ),
                 )
+            except Exception as e:
+                self.disp(msg=_(f"can't retrieve jid: {e}"), error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-            self.host.bridge.profileStartSession(
-                invitation_data["password"],
-                profile,
-                callback=session_started,
-                errback=partial(
-                    self.errback,
-                    msg=_("can't start session: {}"),
-                    exit_code=C.EXIT_BRIDGE_ERRBACK,
-                ),
-            )
-        else:
-            self.output_data(invitation_data)
-
-    def start(self):
-        self.host.bridge.invitationGet(
-            self.args.id,
-            callback=self.invitationGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get invitation data: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+            await self.output_data(invitation_data, jid_)
 
 
 class Modify(base.CommandBase):
@@ -233,7 +218,6 @@
         base.CommandBase.__init__(
             self, host, "modify", use_profile=False, help=_("modify existing invitation")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -283,17 +267,7 @@
             "id", help=_("invitation UUID")
         )
 
-    def invitationModifyCb(self):
-        self.disp(_("invitations have been modified correctly"))
-        self.host.quit(C.EXIT_OK)
-
-    def invitationModifyEb(self, failure_):
-        self.disp(
-            "can't create invitation: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
+    async def start(self):
         extra = dict(self.args.extra)
         for arg_name in ("name", "host_name", "email", "language", "profile"):
             value = getattr(self.args, arg_name)
@@ -301,18 +275,23 @@
                 continue
             if arg_name in extra:
                 self.parser.error(
-                    _(
-                        "you can't set {arg_name} in both optional argument and extra"
-                    ).format(arg_name=arg_name)
+                    _(f"you can't set {arg_name} in both optional argument and extra")
                 )
             extra[arg_name] = value
-        self.host.bridge.invitationModify(
-            self.args.id,
-            extra,
-            self.args.replace,
-            callback=self.invitationModifyCb,
-            errback=self.invitationModifyEb,
-        )
+        try:
+            await self.host.bridge.invitationModify(
+                self.args.id,
+                extra,
+                self.args.replace,
+            )
+        except Exception as e:
+            self.disp(
+                f"can't modify invitation: {e}", error=True
+            )
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("invitations have been modified successfuly"))
+            self.host.quit(C.EXIT_OK)
 
 
 class List(base.CommandBase):
@@ -327,7 +306,6 @@
             extra_outputs=extra_outputs,
             help=_("list invitations data"),
         )
-        self.need_loop = True
 
     def default_output(self, data):
         for idx, datum in enumerate(data.items()):
@@ -347,20 +325,17 @@
             help=_("return only invitations linked to this profile"),
         )
 
-    def invitationListCb(self, data):
-        self.output(data)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.invitationList(
-            self.args.profile,
-            callback=self.invitationListCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't list invitations: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            data = await self.host.bridge.invitationList(
+                self.args.profile,
+            )
+        except Exception as e:
+            self.disp(f"return only invitations linked to this profile: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(data)
+            self.host.quit()
 
 
 class Invitation(base.CommandBase):
--- a/sat_frontends/jp/cmd_merge_request.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_merge_request.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,19 +18,19 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+import os.path
 from . import base
 from sat.core.i18n import _
 from sat.tools.common import data_format
 from sat_frontends.jp.constants import Const as C
 from sat_frontends.jp import xmlui_manager
 from sat_frontends.jp import common
-from functools import partial
-import os.path
 
 __commands__ = ["MergeRequest"]
 
 
 class Set(base.CommandBase):
+
     def __init__(self, host):
         base.CommandBase.__init__(
             self,
@@ -40,7 +40,6 @@
             pubsub_defaults={"service": _("auto"), "node": _("auto")},
             help=_("publish or update a merge request"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -70,47 +69,43 @@
             help=_("labels to categorize your request"),
         )
 
-    def mergeRequestSetCb(self, published_id):
-        if published_id:
-            self.disp("Merge request published at {pub_id}".format(pub_id=published_id))
-        else:
-            self.disp("Merge request published")
-        self.host.quit(C.EXIT_OK)
+    async def start(self):
+        self.repository = os.path.expanduser(os.path.abspath(self.args.repository))
+        await common.fill_well_known_uri(self, self.repository, "merge requests")
+        if not self.args.force:
+            message = _(
+                f"You are going to publish your changes to service "
+                f"[{self.args.service}], are you sure ?"
+            )
+            await self.host.confirmOrQuit(
+                message, _("merge request publication cancelled"))
 
-    def sendRequest(self):
         extra = {"update": True} if self.args.item else {}
         values = {}
         if self.args.labels is not None:
             values["labels"] = self.args.labels
-        self.host.bridge.mergeRequestSet(
-            self.args.service,
-            self.args.node,
-            self.repository,
-            "auto",
-            values,
-            "",
-            self.args.item,
-            data_format.serialise(extra),
-            self.profile,
-            callback=self.mergeRequestSetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't create merge request: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            published_id = await self.host.bridge.mergeRequestSet(
+                self.args.service,
+                self.args.node,
+                self.repository,
+                "auto",
+                values,
+                "",
+                self.args.item,
+                data_format.serialise(extra),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't create merge requests: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def askConfirmation(self):
-        if not self.args.force:
-            message = _(
-                "You are going to publish your changes to service [{service}], are you sure ?"
-            ).format(service=self.args.service)
-            self.host.confirmOrQuit(message, _("merge request publication cancelled"))
-        self.sendRequest()
+        if published_id:
+            self.disp(_(f"Merge request published at {published_id}"))
+        else:
+            self.disp(_("Merge request published"))
 
-    def start(self):
-        self.repository = os.path.expanduser(os.path.abspath(self.args.repository))
-        common.URIFinder(self, self.repository, "merge requests", self.askConfirmation)
+        self.host.quit(C.EXIT_OK)
 
 
 class Get(base.CommandBase):
@@ -125,45 +120,38 @@
             pubsub_defaults={"service": _("auto"), "node": _("auto")},
             help=_("get a merge request"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def mergeRequestGetCb(self, requests_data):
+    async def start(self):
+        await common.fill_well_known_uri(
+            self, os.getcwd(), "merge requests", meta_map={})
+        extra = {}
+        try:
+            requests_data = await self.host.bridge.mergeRequestsGet(
+                self.args.service,
+                self.args.node,
+                self.args.max,
+                self.args.items,
+                "",
+                extra,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get merge request: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
         if self.verbosity >= 1:
             whitelist = None
         else:
             whitelist = {"id", "title", "body"}
         for request_xmlui in requests_data[0]:
             xmlui = xmlui_manager.create(self.host, request_xmlui, whitelist=whitelist)
-            xmlui.show(values_only=True)
+            await xmlui.show(values_only=True)
             self.disp("")
         self.host.quit(C.EXIT_OK)
 
-    def getRequests(self):
-        extra = {}
-        self.host.bridge.mergeRequestsGet(
-            self.args.service,
-            self.args.node,
-            self.args.max,
-            self.args.items,
-            "",
-            extra,
-            self.profile,
-            callback=self.mergeRequestGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get merge request: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
-    def start(self):
-        common.URIFinder(
-            self, os.getcwd(), "merge requests", self.getRequests, meta_map={}
-        )
-
 
 class Import(base.CommandBase):
     def __init__(self, host):
@@ -176,7 +164,6 @@
             pubsub_defaults={"service": _("auto"), "node": _("auto")},
             help=_("import a merge request"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -187,31 +174,25 @@
             help=_("path of the repository (DEFAULT: current directory)"),
         )
 
-    def mergeRequestImportCb(self):
-        self.host.quit(C.EXIT_OK)
-
-    def importRequest(self):
+    async def start(self):
+        self.repository = os.path.expanduser(os.path.abspath(self.args.repository))
+        await common.fill_well_known_uri(
+            self, self.repository, "merge requests", meta_map={})
         extra = {}
-        self.host.bridge.mergeRequestsImport(
-            self.repository,
-            self.args.item,
-            self.args.service,
-            self.args.node,
-            extra,
-            self.profile,
-            callback=self.mergeRequestImportCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't import merge request: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
-    def start(self):
-        self.repository = os.path.expanduser(os.path.abspath(self.args.repository))
-        common.URIFinder(
-            self, self.repository, "merge requests", self.importRequest, meta_map={}
-        )
+        try:
+            await self.host.bridge.mergeRequestsImport(
+                self.repository,
+                self.args.item,
+                self.args.service,
+                self.args.node,
+                extra,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't import merge request: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class MergeRequest(base.CommandBase):
--- a/sat_frontends/jp/cmd_message.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_message.py	Wed Sep 25 08:56:41 2019 +0200
@@ -25,7 +25,6 @@
 from sat.tools.utils import clean_ustr
 from sat.tools.common import data_format
 from sat.tools.common.ansi import ANSI as A
-from functools import partial
 
 __commands__ = ["Message"]
 
@@ -33,7 +32,6 @@
 class Send(base.CommandBase):
     def __init__(self, host):
         super(Send, self).__init__(host, "send", help=_("send a message to a contact"))
-        self.need_loop=True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -84,25 +82,15 @@
             "jid", help=_("the destination jid")
         )
 
-    def multi_send_cb(self):
-        self.sent += 1
-        if self.sent == self.to_send:
-            self.host.quit(self.errcode)
-
-    def multi_send_eb(self, failure_, msg):
-        self.disp(_("Can't send message [{msg}]: {reason}").format(
-            msg=msg, reason=failure_))
-        self.errcode = C.EXIT_BRIDGE_ERRBACK
-        self.multi_send_cb()
-
-    def sendStdin(self, dest_jid):
+    async def sendStdin(self, dest_jid):
         """Send incomming data on stdin to jabber contact
 
         @param dest_jid: destination jid
         """
         header = "\n" if self.args.new_line else ""
+        # FIXME: stdin is not read asynchronously at the moment
         stdin_lines = [
-            stream.decode("utf-8", "ignore") for stream in sys.stdin.readlines()
+            stream for stream in sys.stdin.readlines()
         ]
         extra = {}
         if self.args.subject is None:
@@ -113,99 +101,98 @@
         if self.args.xhtml or self.args.rich:
             key = "xhtml" if self.args.xhtml else "rich"
             if self.args.lang:
-                key = "{}_{}".format(key, self.args.lang)
+                key = f"{key}_{self.args.lang}"
             extra[key] = clean_ustr("".join(stdin_lines))
             stdin_lines = []
 
-        if self.args.separate:  # we send stdin in several messages
-            self.to_send = 0
-            self.sent = 0
-            self.errcode = 0
+        to_send = []
+
+        error = False
 
+        if self.args.separate:
+            # we send stdin in several messages
             if header:
-                self.to_send += 1
-                self.host.bridge.messageSend(
+                # first we sent the header
+                try:
+                    await self.host.bridge.messageSend(
+                        dest_jid,
+                        {self.args.lang: header},
+                        subject,
+                        self.args.type,
+                        profile_key=self.profile,
+                    )
+                except Exception as e:
+                    self.disp(f"can't send header: {e}", error=True)
+                    error = True
+
+            to_send.extend({self.args.lang: clean_ustr(l.replace("\n", ""))}
+                           for l in stdin_lines)
+        else:
+            # we sent all in a single message
+            if not (self.args.xhtml or self.args.rich):
+                msg = {self.args.lang: header + clean_ustr("".join(stdin_lines))}
+            else:
+                msg = {}
+            to_send.append(msg)
+
+        for msg in to_send:
+            try:
+                await self.host.bridge.messageSend(
                     dest_jid,
-                    {self.args.lang: header},
-                    subject,
-                    self.args.type,
-                    profile_key=self.profile,
-                    callback=lambda: None,
-                    errback=lambda ignore: ignore,
-                )
-
-            self.to_send += len(stdin_lines)
-            for line in stdin_lines:
-                self.host.bridge.messageSend(
-                    dest_jid,
-                    {self.args.lang: line.replace("\n", "")},
+                    msg,
                     subject,
                     self.args.type,
                     extra,
-                    profile_key=self.host.profile,
-                    callback=self.multi_send_cb,
-                    errback=partial(self.multi_send_eb, msg=line),
-                )
+                    profile_key=self.host.profile)
+            except Exception as e:
+                self.disp(f"can't send message {msg!r}: {e}", error=True)
+                error = True
 
-        else:
-            msg = (
-                {self.args.lang: header + clean_ustr("".join(stdin_lines))}
-                if not (self.args.xhtml or self.args.rich)
-                else {}
-            )
-            self.host.bridge.messageSend(
-                dest_jid,
-                msg,
-                subject,
-                self.args.type,
-                extra,
-                profile_key=self.host.profile,
-                callback=self.host.quit,
-                errback=partial(self.errback,
-                                msg=_("Can't send message: {}")))
+        if error:
+            # at least one message sending failed
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def encryptionNamespaceGetCb(self, namespace, jid_):
-        self.host.bridge.messageEncryptionStart(
-            jid_, namespace, not self.args.encrypt_noreplace,
-            self.profile,
-            callback=lambda: self.sendStdin(jid_),
-            errback=partial(self.errback,
-                            msg=_("Can't start encryption session: {}"),
-                            exit_code=C.EXIT_BRIDGE_ERRBACK,
-                            ))
+        self.host.quit()
 
-
-    def start(self):
+    async def start(self):
         if self.args.xhtml and self.args.separate:
             self.disp(
                 "argument -s/--separate is not compatible yet with argument -x/--xhtml",
                 error=True,
             )
-            self.host.quit(2)
+            self.host.quit(C.EXIT_BAD_ARG)
 
-        jids = self.host.check_jids([self.args.jid])
+        jids = await self.host.check_jids([self.args.jid])
         jid_ = jids[0]
 
         if self.args.encrypt_noreplace and self.args.encrypt is None:
             self.parser.error("You need to use --encrypt if you use --encrypt-noreplace")
 
         if self.args.encrypt is not None:
-            self.host.bridge.encryptionNamespaceGet(self.args.encrypt,
-                callback=partial(self.encryptionNamespaceGetCb, jid_=jid_),
-                errback=partial(self.errback,
-                                msg=_("Can't get encryption namespace: {}"),
-                                exit_code=C.EXIT_BRIDGE_ERRBACK,
-                                ))
-        else:
-            self.sendStdin(jid_)
+            try:
+                namespace = await self.host.bridge.encryptionNamespaceGet(
+                    self.args.encrypt)
+            except Exception as e:
+                self.disp(f"can't get encryption namespace: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+            try:
+                await self.host.bridge.messageEncryptionStart(
+                    jid_, namespace, not self.args.encrypt_noreplace, self.profile
+                )
+            except Exception as e:
+                self.disp(f"can't start encryption session: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        await self.sendStdin(jid_)
 
 
 class MAM(base.CommandBase):
 
     def __init__(self, host):
         super(MAM, self).__init__(
-            host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True, help=_("query archives using MAM"))
-        self.need_loop=True
+            host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True,
+            help=_("query archives using MAM"))
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -235,9 +222,37 @@
             "--index", dest="rsm_index", type=int,
             help=_("index of the page to retrieve"))
 
-    def _sessionInfosGetCb(self, session_info, data, metadata):
+    async def start(self):
+        extra = {}
+        if self.args.mam_start is not None:
+            extra["mam_start"] = float(self.args.mam_start)
+        if self.args.mam_end is not None:
+            extra["mam_end"] = float(self.args.mam_end)
+        if self.args.mam_with is not None:
+            extra["mam_with"] = self.args.mam_with
+        for suff in ('max', 'after', 'before', 'index'):
+            key = 'rsm_' + suff
+            value = getattr(self.args,key)
+            if value is not None:
+                extra[key] = str(value)
+        try:
+            data, metadata, profile = await self.host.bridge.MAMGet(
+                self.args.service, data_format.serialise(extra), self.profile)
+        except Exception as e:
+            self.disp(f"can't retrieve MAM archives: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        try:
+            session_info = await self.host.bridge.sessionInfosGet(self.profile)
+        except Exception as e:
+            self.disp(f"can't get session infos: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        # we need to fill own_jid for message output
         self.host.own_jid = jid.JID(session_info["jid"])
-        self.output(data)
+
+        await self.output(data)
+
         # FIXME: metadata are not displayed correctly and don't play nice with output
         #        they should be added to output data somehow
         if self.verbosity:
@@ -250,29 +265,6 @@
 
         self.host.quit()
 
-    def _MAMGetCb(self, result):
-        data, metadata, profile = result
-        self.host.bridge.sessionInfosGet(self.profile,
-            callback=partial(self._sessionInfosGetCb, data=data, metadata=metadata),
-            errback=self.errback)
-
-    def start(self):
-        extra = {}
-        if self.args.mam_start is not None:
-            extra["mam_start"] = float(self.args.mam_start)
-        if self.args.mam_end is not None:
-            extra["mam_end"] = float(self.args.mam_end)
-        if self.args.mam_with is not None:
-            extra["mam_with"] = self.args.mam_with
-        for suff in ('max', 'after', 'before', 'index'):
-            key = 'rsm_' + suff
-            value = getattr(self.args,key)
-            if value is not None:
-                extra[key] = str(value)
-        self.host.bridge.MAMGet(
-            self.args.service, data_format.serialise(extra), self.profile,
-            callback=self._MAMGetCb, errback=self.errback)
-
 
 class Message(base.CommandBase):
     subcommands = (Send, MAM)
--- a/sat_frontends/jp/cmd_param.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_param.py	Wed Sep 25 08:56:41 2019 +0200
@@ -21,87 +21,118 @@
 
 from . import base
 from sat.core.i18n import _
+from .constants import Const as C
 __commands__ = ["Param"]
 
 
 class Get(base.CommandBase):
     def __init__(self, host):
-        super(Get, self).__init__(host, 'get', need_connect=False, help=_('Get a parameter value'))
+        super(Get, self).__init__(
+            host, 'get', need_connect=False, help=_('get a parameter value'))
 
     def add_parser_options(self):
-        self.parser.add_argument("category", nargs='?', help=_("Category of the parameter"))
-        self.parser.add_argument("name", nargs='?', help=_("Name of the parameter"))
-        self.parser.add_argument("-a", "--attribute", type=str, default="value", help=_("Name of the attribute to get"))
-        self.parser.add_argument("--security-limit", type=int, default=-1, help=_("Security limit"))
+        self.parser.add_argument(
+            "category", nargs='?', help=_("category of the parameter"))
+        self.parser.add_argument("name", nargs='?', help=_("name of the parameter"))
+        self.parser.add_argument(
+            "-a", "--attribute", type=str, default="value",
+            help=_("name of the attribute to get"))
+        self.parser.add_argument(
+            "--security-limit", type=int, default=-1, help=_("security limit"))
 
-    def start(self):
+    async def start(self):
         if self.args.category is None:
-            categories = self.host.bridge.getParamsCategories()
+            categories = await self.host.bridge.getParamsCategories()
             print("\n".join(categories))
         elif self.args.name is None:
             try:
-                values_dict = self.host.bridge.asyncGetParamsValuesFromCategory(self.args.category, self.args.security_limit, self.profile)
+                values_dict = await self.host.bridge.asyncGetParamsValuesFromCategory(
+                    self.args.category, self.args.security_limit, self.profile)
             except Exception as e:
-                print("Can't find requested parameters: {}".format(e))
-                self.host.quit(1)
-            for name, value in values_dict.items():
-                print("{}\t{}".format(name, value))
+                self.disp(_(f"can't find requested parameters: {e}"), error=True)
+                self.host.quit(C.EXIT_NOT_FOUND)
+            else:
+                for name, value in values_dict.items():
+                    print(f"{name}\t{value}")
         else:
             try:
-                value = self.host.bridge.asyncGetParamA(self.args.name, self.args.category, self.args.attribute,
-                                                                  self.args.security_limit, self.profile)
+                value = await self.host.bridge.asyncGetParamA(
+                    self.args.name, self.args.category, self.args.attribute,
+                    self.args.security_limit, self.profile)
             except Exception as e:
-                print("Can't find requested parameter: {}".format(e))
-                self.host.quit(1)
-            print(value)
+                self.disp(_(f"can't find requested parameter: {e}"), error=True)
+                self.host.quit(C.EXIT_NOT_FOUND)
+            else:
+                print(value)
+        self.host.quit()
 
 
 class Set(base.CommandBase):
     def __init__(self, host):
-        super(Set, self).__init__(host, 'set', need_connect=False, help=_('Set a parameter value'))
+        super(Set, self).__init__(host, 'set', need_connect=False, help=_('set a parameter value'))
 
     def add_parser_options(self):
-        self.parser.add_argument("category", help=_("Category of the parameter"))
-        self.parser.add_argument("name", help=_("Name of the parameter"))
-        self.parser.add_argument("value", help=_("Name of the parameter"))
-        self.parser.add_argument("--security-limit", type=int, default=-1, help=_("Security limit"))
+        self.parser.add_argument("category", help=_("category of the parameter"))
+        self.parser.add_argument("name", help=_("name of the parameter"))
+        self.parser.add_argument("value", help=_("name of the parameter"))
+        self.parser.add_argument("--security-limit", type=int, default=-1, help=_("security limit"))
 
-    def start(self):
+    async def start(self):
         try:
-            self.host.bridge.setParam(self.args.name, self.args.value, self.args.category, self.args.security_limit, self.profile)
+            await self.host.bridge.setParam(
+                self.args.name, self.args.value, self.args.category,
+                self.args.security_limit, self.profile)
         except Exception as e:
-            print("Can set requested parameter: {}".format(e))
+            self.disp(_(f"can't set requested parameter: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class SaveTemplate(base.CommandBase):
+    # FIXME: this should probably be removed, it's not used and not useful for end-user
+
     def __init__(self, host):
-        super(SaveTemplate, self).__init__(host, 'save', use_profile=False, help=_('Save parameters template to xml file'))
+        super(SaveTemplate, self).__init__(
+            host, 'save', use_profile=False,
+            help=_('save parameters template to xml file'))
 
     def add_parser_options(self):
-        self.parser.add_argument("filename", type=str, help=_("Output file"))
+        self.parser.add_argument("filename", type=str, help=_("output file"))
 
-    def start(self):
-        """Save parameters template to xml file"""
-        if self.host.bridge.saveParamsTemplate(self.args.filename):
-            print(_("Parameters saved to file %s") % self.args.filename)
+    async def start(self):
+        """Save parameters template to XML file"""
+        try:
+            await self.host.bridge.saveParamsTemplate(self.args.filename)
+        except Exception as e:
+            self.disp(_(f"can't save parameters to file: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
-            print(_("Can't save parameters to file %s") % self.args.filename)
+            self.disp(_(f"parameters saved to file {self.args.filename}"))
+            self.host.quit()
 
 
 class LoadTemplate(base.CommandBase):
+    # FIXME: this should probably be removed, it's not used and not useful for end-user
 
     def __init__(self, host):
-        super(LoadTemplate, self).__init__(host, 'load', use_profile=False, help=_('Load parameters template from xml file'))
+        super(LoadTemplate, self).__init__(
+            host, 'load', use_profile=False,
+            help=_('load parameters template from xml file'))
 
     def add_parser_options(self):
-        self.parser.add_argument("filename", type=str, help=_("Input file"))
+        self.parser.add_argument("filename", type=str, help=_("input file"))
 
-    def start(self):
+    async def start(self):
         """Load parameters template from xml file"""
-        if self.host.bridge.loadParamsTemplate(self.args.filename):
-            print(_("Parameters loaded from file %s") % self.args.filename)
+        try:
+            self.host.bridge.loadParamsTemplate(self.args.filename)
+        except Exception as e:
+            self.disp(_(f"can't load parameters from file: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
-            print(_("Can't load parameters from file %s") % self.args.filename)
+            self.disp(_(f"parameters loaded from file {self.args.filename}"))
+            self.host.quit()
 
 
 class Param(base.CommandBase):
--- a/sat_frontends/jp/cmd_ping.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_ping.py	Wed Sep 25 08:56:41 2019 +0200
@@ -19,6 +19,7 @@
 
 from . import base
 from sat.core.i18n import _
+from sat_frontends.jp.constants import Const as C
 
 __commands__ = ["Ping"]
 
@@ -27,7 +28,6 @@
 
     def __init__(self, host):
         super(Ping, self).__init__(host, 'ping', help=_('ping XMPP entity'))
-        self.need_loop=True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -37,11 +37,13 @@
             "-d", "--delay-only", action="store_true", help=_("output delay only (in s)")
         )
 
-    def _pingCb(self, pong_time):
-        fmt = "{time}" if self.args.delay_only else  "PONG ({time} s)"
-        self.disp(fmt.format(time=pong_time))
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.ping(self.args.jid, self.profile,
-            callback=self._pingCb, errback=self.errback)
+    async def start(self):
+        try:
+            pong_time = await self.host.bridge.ping(self.args.jid, self.profile)
+        except Exception as e:
+            self.disp(msg=_(f"can't do the ping: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            msg = pong_time if self.args.delay_only else  f"PONG ({pong_time} s)"
+            self.disp(msg)
+            self.host.quit()
--- a/sat_frontends/jp/cmd_pipe.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_pipe.py	Wed Sep 25 08:56:41 2019 +0200
@@ -17,17 +17,16 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import socket
+import asyncio
+import errno
+from functools import partial
 from sat_frontends.jp import base
-
 from sat_frontends.jp.constants import Const as C
+from sat_frontends.jp import xmlui_manager
 import sys
 from sat.core.i18n import _
 from sat_frontends.tools import jid
-import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
-from functools import partial
-import socket
-import socketserver
-import errno
 
 __commands__ = ["Pipe"]
 
@@ -37,59 +36,60 @@
 class PipeOut(base.CommandBase):
     def __init__(self, host):
         super(PipeOut, self).__init__(host, "out", help=_("send a pipe a stream"))
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
             "jid", help=_("the destination jid")
         )
 
-    def streamOutCb(self, port):
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        s.connect(("127.0.0.1", int(port)))
-        while True:
-            buf = sys.stdin.read(4096)
-            if not buf:
-                break
-            try:
-                s.sendall(buf)
-            except socket.error as e:
-                if e.errno == errno.EPIPE:
-                    sys.stderr.write(str(e) + "\n")
-                    self.host.quit(1)
-                else:
-                    raise e
-        self.host.quit()
+    async def start(self):
+        """ Create named pipe, and send stdin to it """
+        try:
+            port = await self.host.bridge.streamOut(
+                await self.host.get_full_jid(self.args.jid),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't start stream: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            # FIXME: we use temporarily blocking code here, as it simplify
+            #        asyncio port: "loop.connect_read_pipe(lambda: reader_protocol,
+            #        sys.stdin.buffer)" doesn't work properly when a file is piped in
+            #        (we get a "ValueError: Pipe transport is for pipes/sockets only.")
+            #        while it's working well for simple text sending.
 
-    def start(self):
-        """ Create named pipe, and send stdin to it """
-        self.host.bridge.streamOut(
-            self.host.get_full_jid(self.args.jid),
-            self.profile,
-            callback=self.streamOutCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't start stream: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            s.connect(("127.0.0.1", int(port)))
+
+            while True:
+                buf = sys.stdin.buffer.read(4096)
+                if not buf:
+                    break
+                try:
+                    s.sendall(buf)
+                except socket.error as e:
+                    if e.errno == errno.EPIPE:
+                        sys.stderr.write(f"e\n")
+                        self.host.quit(1)
+                    else:
+                        raise e
+            self.host.quit()
 
 
-class StreamServer(socketserver.BaseRequestHandler):
-    def handle(self):
-        while True:
-            data = self.request.recv(4096)
-            if not data:
-                break
-            sys.stdout.write(data)
-            try:
-                sys.stdout.flush()
-            except IOError as e:
-                sys.stderr.write(str(e) + "\n")
-                break
-        #  calling shutdown will do a deadlock as we don't use separate thread
-        # this is a workaround (cf. https://stackoverflow.com/a/36017741)
-        self.server._BaseServer__shutdown_request = True
+async def handle_stream_in(reader, writer, host):
+    """Write all received data to stdout"""
+    while True:
+        data = await reader.read(4096)
+        if not data:
+            break
+        sys.stdout.buffer.write(data)
+        try:
+            sys.stdout.flush()
+        except IOError as e:
+            sys.stderr.write(f"{e}\n")
+            break
+    host.quitFromSignal()
 
 
 class PipeIn(base.CommandAnswering):
@@ -105,35 +105,33 @@
         )
 
     def getXmluiId(self, action_data):
-        # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
-        #        should be available in the future
-        # TODO: XMLUI module
         try:
             xml_ui = action_data["xmlui"]
         except KeyError:
             self.disp(_("Action has no XMLUI"), 1)
         else:
-            ui = ET.fromstring(xml_ui.encode("utf-8"))
-            xmlui_id = ui.get("submit")
-            if not xmlui_id:
+            ui = xmlui_manager.create(self.host, xml_ui)
+            if not ui.submit_id:
                 self.disp(_("Invalid XMLUI received"), error=True)
-            return xmlui_id
+                self.quitFromSignal(C.EXIT_INTERNAL_ERROR)
+            return ui.submit_id
 
-    def onStreamAction(self, action_data, action_id, security_limit, profile):
+    async def onStreamAction(self, action_data, action_id, security_limit, profile):
         xmlui_id = self.getXmluiId(action_data)
         if xmlui_id is None:
-            return self.host.quitFromSignal(1)
+            self.host.quitFromSignal(C.EXIT_ERROR)
         try:
             from_jid = jid.JID(action_data["meta_from_jid"])
         except KeyError:
-            self.disp(_("Ignoring action without from_jid data"), 1)
+            self.disp(_("Ignoring action without from_jid data"), error=True)
             return
 
         if not self.bare_jids or from_jid.bare in self.bare_jids:
             host, port = "localhost", START_PORT
             while True:
                 try:
-                    server = socketserver.TCPServer((host, port), StreamServer)
+                    server = await asyncio.start_server(
+                        partial(handle_stream_in, host=self.host), host, port)
                 except socket.error as e:
                     if e.errno == errno.EADDRINUSE:
                         port += 1
@@ -142,12 +140,15 @@
                 else:
                     break
             xmlui_data = {"answer": C.BOOL_TRUE, "port": str(port)}
-            self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
-            server.serve_forever()
+            await self.host.bridge.launchAction(
+                xmlui_id, xmlui_data, profile_key=profile)
+            async with server:
+                await server.serve_forever()
             self.host.quitFromSignal()
 
-    def start(self):
+    async def start(self):
         self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids]
+        await self.start_answering()
 
 
 class Pipe(base.CommandBase):
--- a/sat_frontends/jp/cmd_profile.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_profile.py	Wed Sep 25 08:56:41 2019 +0200
@@ -25,7 +25,6 @@
 log = getLogger(__name__)
 from sat.core.i18n import _
 from sat_frontends.jp import base
-from functools import partial
 
 __commands__ = ["Profile"]
 
@@ -43,18 +42,73 @@
     def add_parser_options(self):
         pass
 
+    async def start(self):
+        # connection is already managed by profile common commands
+        # so we just need to check arguments and quit
+        if not self.args.connect and not self.args.start_session:
+            self.parser.error(_("You need to use either --connect or --start-session"))
+        self.host.quit()
 
 class ProfileDisconnect(base.CommandBase):
 
     def __init__(self, host):
         super(ProfileDisconnect, self).__init__(host, 'disconnect', need_connect=False, help=('disconnect a profile'))
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def start(self):
-        self.host.bridge.disconnect(self.args.profile, callback=self.host.quit)
+    async def start(self):
+        try:
+            await self.host.bridge.disconnect(self.args.profile)
+        except Exception as e:
+            self.disp(f"can't disconnect profile: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
+
+
+class ProfileCreate(base.CommandBase):
+    def __init__(self, host):
+        super(ProfileCreate, self).__init__(host, 'create', use_profile=False, help=('create a new profile'))
+
+    def add_parser_options(self):
+        self.parser.add_argument('profile', type=str, help=_('the name of the profile'))
+        self.parser.add_argument('-p', '--password', type=str, default='', help=_('the password of the profile'))
+        self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile'))
+        self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'),
+                                 metavar='PASSWORD')
+        self.parser.add_argument('-C', '--component', default='',
+                                 help=_('set to component import name (entry point) if this is a component'))
+
+    async def start(self):
+        """Create a new profile"""
+        if self.args.profile in await self.host.bridge.profilesListGet():
+            self.disp(f"Profile {self.args.profile} already exists.", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERROR)
+        try:
+            await self.host.bridge.profileCreate(
+                self.args.profile, self.args.password, self.args.component)
+        except Exception as e:
+            self.disp(f"can't create profile: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        try:
+            await self.host.bridge.profileStartSession(
+                self.args.password, self.args.profile)
+        except Exception as e:
+            self.disp(f"can't start profile session: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+        if self.args.jid:
+            await self.host.bridge.setParam(
+                "JabberID", self.args.jid, "Connection", profile_key=self.args.profile)
+        xmpp_pwd = self.args.password or self.args.xmpp_password
+        if xmpp_pwd:
+            await self.host.bridge.setParam(
+                "Password", xmpp_pwd, "Connection", profile_key=self.args.profile)
+
+        self.disp(f'profile {self.args.profile} created successfully', 1)
+        self.host.quit()
 
 
 class ProfileDefault(base.CommandBase):
@@ -64,8 +118,9 @@
     def add_parser_options(self):
         pass
 
-    def start(self):
-        print(self.host.bridge.profileNameGet('@DEFAULT@'))
+    async def start(self):
+        print(await self.host.bridge.profileNameGet('@DEFAULT@'))
+        self.host.quit()
 
 
 class ProfileDelete(base.CommandBase):
@@ -76,47 +131,51 @@
         self.parser.add_argument('profile', type=str, help=PROFILE_HELP)
         self.parser.add_argument('-f', '--force', action='store_true', help=_('delete profile without confirmation'))
 
-    def start(self):
-        if self.args.profile not in self.host.bridge.profilesListGet():
-            log.error("Profile %s doesn't exist." % self.args.profile)
-            self.host.quit(1)
+    async def start(self):
+        if self.args.profile not in await self.host.bridge.profilesListGet():
+            log.error(f"Profile {self.args.profile} doesn't exist.")
+            self.host.quit(C.EXIT_NOT_FOUND)
         if not self.args.force:
-            message = "Are you sure to delete profile [{}] ?".format(self.args.profile)
-            res = input("{} (y/N)? ".format(message))
-            if res not in ("y", "Y"):
-                self.disp(_("Profile deletion cancelled"))
-                self.host.quit(2)
+            message = f"Are you sure to delete profile [{self.args.profile}] ?"
+            cancel_message = "Profile deletion cancelled"
+            await self.host.confirmOrQuit(message, cancel_message)
 
-        self.host.bridge.asyncDeleteProfile(self.args.profile, callback=lambda __: None)
+        await self.host.bridge.asyncDeleteProfile(self.args.profile)
+        self.host.quit()
 
 
 class ProfileInfo(base.CommandBase):
     def __init__(self, host):
         super(ProfileInfo, self).__init__(host, 'info', need_connect=False, help=_('get information about a profile'))
-        self.need_loop = True
         self.to_show = [(_("jid"), "Connection", "JabberID"),]
         self.largest = max([len(item[0]) for item in self.to_show])
 
-
     def add_parser_options(self):
         self.parser.add_argument('--show-password', action='store_true', help=_('show the XMPP password IN CLEAR TEXT'))
 
-    def showNextValue(self, label=None, category=None, value=None):
+    async def showNextValue(self, label=None, category=None, value=None):
         """Show next value from self.to_show and quit on last one"""
         if label is not None:
-            print((("{label:<"+str(self.largest+2)+"}{value}").format(label=label+": ", value=value)))
+            print((("{label:<"+str(self.largest+2)+"}{value}").format(
+                label=label+": ", value=value)))
         try:
             label, category, name = self.to_show.pop(0)
         except IndexError:
             self.host.quit()
         else:
-            self.host.bridge.asyncGetParamA(name, category, profile_key=self.host.profile,
-                                            callback=lambda value: self.showNextValue(label, category, value))
+            try:
+                value = await self.host.bridge.asyncGetParamA(
+                    name, category, profile_key=self.host.profile)
+            except Exception as e:
+                self.disp(f"can't get {name}/{category} param: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+            else:
+                await self.showNextValue(label, category, value)
 
-    def start(self):
+    async def start(self):
         if self.args.show_password:
             self.to_show.append((_("XMPP password"), "Connection", "Password"))
-        self.showNextValue()
+        await self.showNextValue()
 
 
 class ProfileList(base.CommandBase):
@@ -129,54 +188,19 @@
         group.add_argument('-C', '--components', action='store_true', help=('get components profiles only'))
 
 
-    def start(self):
+    async def start(self):
         if self.args.clients:
             clients, components = True, False
         elif self.args.components:
             clients, components = False, True
         else:
             clients, components = True, True
-        self.output(self.host.bridge.profilesListGet(clients, components))
-
-
-class ProfileCreate(base.CommandBase):
-    def __init__(self, host):
-        super(ProfileCreate, self).__init__(host, 'create', use_profile=False, help=('create a new profile'))
-        self.need_loop = True
-
-    def add_parser_options(self):
-        self.parser.add_argument('profile', type=str, help=_('the name of the profile'))
-        self.parser.add_argument('-p', '--password', type=str, default='', help=_('the password of the profile'))
-        self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile'))
-        self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'),
-                                 metavar='PASSWORD')
-        self.parser.add_argument('-C', '--component', default='',
-                                 help=_('set to component import name (entry point) if this is a component'))
-
-    def _session_started(self, __):
-        if self.args.jid:
-            self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.args.profile)
-        xmpp_pwd = self.args.password or self.args.xmpp_password
-        if xmpp_pwd:
-            self.host.bridge.setParam("Password", xmpp_pwd, "Connection", profile_key=self.args.profile)
+        await self.output(await self.host.bridge.profilesListGet(clients, components))
         self.host.quit()
 
-    def _profile_created(self):
-        self.host.bridge.profileStartSession(self.args.password, self.args.profile, callback=self._session_started, errback=None)
-
-    def start(self):
-        """Create a new profile"""
-        if self.args.profile in self.host.bridge.profilesListGet():
-            log.error("Profile %s already exists." % self.args.profile)
-            self.host.quit(1)
-        self.host.bridge.profileCreate(self.args.profile, self.args.password, self.args.component,
-                                       callback=self._profile_created,
-                                       errback=partial(self.errback,
-                                                       msg=_("can't create profile: {}"),
-                                                       exit_code=C.EXIT_BRIDGE_ERRBACK))
-
 
 class ProfileModify(base.CommandBase):
+
     def __init__(self, host):
         super(ProfileModify, self).__init__(host, 'modify', need_connect=False, help=_('modify an existing profile'))
 
@@ -189,17 +213,22 @@
                                  metavar='PASSWORD')
         self.parser.add_argument('-D', '--default', action='store_true', help=_('set as default profile'))
 
-    def start(self):
+    async def start(self):
         if self.args.disable_password:
             self.args.password = ''
         if self.args.password is not None:
-            self.host.bridge.setParam("Password", self.args.password, "General", profile_key=self.host.profile)
+            await self.host.bridge.setParam(
+                "Password", self.args.password, "General", profile_key=self.host.profile)
         if self.args.jid is not None:
-            self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.host.profile)
+            await self.host.bridge.setParam(
+                "JabberID", self.args.jid, "Connection", profile_key=self.host.profile)
         if self.args.xmpp_password is not None:
-            self.host.bridge.setParam("Password", self.args.xmpp_password, "Connection", profile_key=self.host.profile)
+            await self.host.bridge.setParam(
+                "Password", self.args.xmpp_password, "Connection", profile_key=self.host.profile)
         if self.args.default:
-            self.host.bridge.profileSetDefault(self.host.profile)
+            await self.host.bridge.profileSetDefault(self.host.profile)
+
+        self.host.quit()
 
 
 class Profile(base.CommandBase):
--- a/sat_frontends/jp/cmd_pubsub.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_pubsub.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,6 +18,12 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+import argparse
+import os.path
+import re
+import sys
+import subprocess
+import asyncio
 from . import base
 from sat.core.i18n import _
 from sat.core import exceptions
@@ -29,11 +35,6 @@
 from sat.tools.common import uri
 from sat.tools.common.ansi import ANSI as A
 from sat_frontends.tools import jid, strings
-import argparse
-import os.path
-import re
-import subprocess
-import sys
 
 __commands__ = ["Pubsub"]
 
@@ -55,7 +56,6 @@
             pubsub_flags={C.NODE},
             help=_("retrieve node configuration"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -72,28 +72,23 @@
     def filterKey(self, key):
         return any((key == k or key == "pubsub#" + k) for k in self.args.keys)
 
-    def psNodeConfigurationGetCb(self, config_dict):
-        key_filter = (lambda k: True) if not self.args.keys else self.filterKey
-        config_dict = {
-            self.removePrefix(k): v for k, v in config_dict.items() if key_filter(k)
-        }
-        self.output(config_dict)
-        self.host.quit()
-
-    def psNodeConfigurationGetEb(self, failure_):
-        self.disp(
-            "can't get node configuration: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.psNodeConfigurationGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psNodeConfigurationGetCb,
-            errback=self.psNodeConfigurationGetEb,
-        )
+    async def start(self):
+        try:
+            config_dict = await self.host.bridge.psNodeConfigurationGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get node configuration: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            key_filter = (lambda k: True) if not self.args.keys else self.filterKey
+            config_dict = {
+                self.removePrefix(k): v for k, v in config_dict.items() if key_filter(k)
+            }
+            await self.output(config_dict)
+            self.host.quit()
 
 
 class NodeCreate(base.CommandBase):
@@ -108,7 +103,6 @@
             use_verbose=True,
             help=_("create a node"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -128,35 +122,28 @@
             help=_('don\'t prepend "pubsub#" prefix to field names'),
         )
 
-    def psNodeCreateCb(self, node_id):
-        if self.host.verbosity:
-            announce = _("node created successfully: ")
-        else:
-            announce = ""
-        self.disp(announce + node_id)
-        self.host.quit()
-
-    def psNodeCreateEb(self, failure_):
-        self.disp("can't create: {reason}".format(reason=failure_), error=True)
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
+    async def start(self):
         if not self.args.full_prefix:
             options = {"pubsub#" + k: v for k, v in self.args.fields}
         else:
             options = dict(self.args.fields)
-        self.host.bridge.psNodeCreate(
-            self.args.service,
-            self.args.node,
-            options,
-            self.profile,
-            callback=self.psNodeCreateCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't create node: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            node_id = await self.host.bridge.psNodeCreate(
+                self.args.service,
+                self.args.node,
+                options,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(msg=_(f"can't create node: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            if self.host.verbosity:
+                announce = _("node created successfully: ")
+            else:
+                announce = ""
+            self.disp(announce + node_id)
+            self.host.quit()
 
 
 class NodePurge(base.CommandBase):
@@ -169,7 +156,6 @@
             pubsub_flags={C.NODE},
             help=_("purge a node (i.e. remove all items from it)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -179,35 +165,30 @@
             help=_("purge node without confirmation"),
         )
 
-    def psNodePurgeCb(self):
-        self.disp(_("node [{node}] purged successfully").format(node=self.args.node))
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         if not self.args.force:
             if not self.args.service:
-                message = _("Are you sure to purge PEP node [{node_id}]? "
-                            "This will delete ALL items from it!").format(
-                    node_id=self.args.node
-                )
+                message = _(
+                    f"Are you sure to purge PEP node [{self.args.node}]? This will "
+                    f"delete ALL items from it!")
             else:
                 message = _(
-                    "Are you sure to delete node [{node_id}] on service [{service}]? "
-                    "This will delete ALL items from it!"
-                ).format(node_id=self.args.node, service=self.args.service)
-            self.host.confirmOrQuit(message, _("node purge cancelled"))
+                    f"Are you sure to delete node [{self.args.node}] on service "
+                    f"[{self.args.service}]? This will delete ALL items from it!")
+            await self.host.confirmOrQuit(message, _("node purge cancelled"))
 
-        self.host.bridge.psNodePurge(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psNodePurgeCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't purge node: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.psNodePurge(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(msg=_(f"can't purge node: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f"node [{self.args.node}] purged successfully"))
+            self.host.quit()
 
 
 class NodeDelete(base.CommandBase):
@@ -220,7 +201,6 @@
             pubsub_flags={C.NODE},
             help=_("delete a node"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -230,33 +210,27 @@
             help=_("delete node without confirmation"),
         )
 
-    def psNodeDeleteCb(self):
-        self.disp(_("node [{node}] deleted successfully").format(node=self.args.node))
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         if not self.args.force:
             if not self.args.service:
-                message = _("Are you sure to delete PEP node [{node_id}] ?").format(
-                    node_id=self.args.node
-                )
+                message = _(f"Are you sure to delete PEP node [{self.args.node}] ?")
             else:
-                message = _(
-                    "Are you sure to delete node [{node_id}] on service [{service}] ?"
-                ).format(node_id=self.args.node, service=self.args.service)
-            self.host.confirmOrQuit(message, _("node deletion cancelled"))
+                message = _(f"Are you sure to delete node [{self.args.node}] on "
+                            f"service [{self.args.service}]?")
+            await self.host.confirmOrQuit(message, _("node deletion cancelled"))
 
-        self.host.bridge.psNodeDelete(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psNodeDeleteCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't delete node: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.psNodeDelete(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't delete node: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f"node [{self.args.node}] deleted successfully"))
+            self.host.quit()
 
 
 class NodeSet(base.CommandBase):
@@ -271,7 +245,6 @@
             use_verbose=True,
             help=_("set node configuration"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -284,32 +257,33 @@
             metavar=("KEY", "VALUE"),
             help=_("configuration field to set (required)"),
         )
-
-    def psNodeConfigurationSetCb(self):
-        self.disp(_("node configuration successful"), 1)
-        self.host.quit()
-
-    def psNodeConfigurationSetEb(self, failure_):
-        self.disp(
-            "can't set node configuration: {reason}".format(reason=failure_), error=True
+        self.parser.add_argument(
+            "-F",
+            "--full-prefix",
+            action="store_true",
+            help=_('don\'t prepend "pubsub#" prefix to field names'),
         )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def getKeyName(self, k):
-        if not k.startswith("pubsub#"):
+        if self.args.full_prefix or k.startswith("pubsub#"):
+            return k
+        else:
             return "pubsub#" + k
-        else:
-            return k
 
-    def start(self):
-        self.host.bridge.psNodeConfigurationSet(
-            self.args.service,
-            self.args.node,
-            {self.getKeyName(k): v for k, v in self.args.fields},
-            self.profile,
-            callback=self.psNodeConfigurationSetCb,
-            errback=self.psNodeConfigurationSetEb,
-        )
+    async def start(self):
+        try:
+            await self.host.bridge.psNodeConfigurationSet(
+                self.args.service,
+                self.args.node,
+                {self.getKeyName(k): v for k, v in self.args.fields},
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set node configuration: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("node configuration successful"), 1)
+            self.host.quit()
 
 
 class NodeImport(base.CommandBase):
@@ -322,7 +296,6 @@
             pubsub_flags={C.NODE},
             help=_("import raw XML to a node"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -337,12 +310,7 @@
                    "whole XML of each item to import."),
         )
 
-    def psItemsSendCb(self, item_ids):
-        self.disp(_('items published with id(s) {item_ids}').format(
-            item_ids=', '.join(item_ids)))
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         try:
             element, etree = xml_tools.etreeParse(self, self.args.import_file,
                                                   reraise=True)
@@ -364,38 +332,34 @@
                 _("You are not using list of pubsub items, we can't import this file"),
                 error=True)
             self.host.quit(C.EXIT_DATA_ERROR)
+            return
 
-        items = [etree.tostring(i, encoding="utf-8") for i in element]
+        items = [etree.tostring(i, encoding="unicode") for i in element]
         if self.args.admin:
-            self.host.bridge.psAdminItemsSend(
+            method = self.host.bridge.psAdminItemsSend
+        else:
+            self.disp(_("Items are imported without using admin mode, publisher can't "
+                        "be changed"))
+            method = self.host.bridge.psItemsSend
+
+        try:
+            items_ids = await method(
                 self.args.service,
                 self.args.node,
                 items,
                 "",
                 self.profile,
-                callback=partial(self.psItemsSendCb),
-                errback=partial(
-                    self.errback,
-                    msg=_("can't send item: {}"),
-                    exit_code=C.EXIT_BRIDGE_ERRBACK,
-                ),
             )
+        except Exception as e:
+            self.disp(f"can't send items: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
-            self.disp(_("Items are imported without using admin mode, publisher can't "
-                        "be changed"))
-            self.host.bridge.psItemsSend(
-                self.args.service,
-                self.args.node,
-                items,
-                "",
-                self.profile,
-                callback=partial(self.psItemsSendCb),
-                errback=partial(
-                    self.errback,
-                    msg=_("can't send item: {}"),
-                    exit_code=C.EXIT_BRIDGE_ERRBACK,
-                ),
-            )
+            if items_ids:
+                self.disp(_('items published with id(s) {items_ids}').format(
+                    items_ids=', '.join(items_ids)))
+            else:
+                self.disp(_('items published'))
+            self.host.quit()
 
 
 class NodeAffiliationsGet(base.CommandBase):
@@ -409,29 +373,23 @@
             pubsub_flags={C.NODE},
             help=_("retrieve node affiliations (for node owner)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psNodeAffiliationsGetCb(self, affiliations):
-        self.output(affiliations)
-        self.host.quit()
-
-    def psNodeAffiliationsGetEb(self, failure_):
-        self.disp(
-            "can't get node affiliations: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.psNodeAffiliationsGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psNodeAffiliationsGetCb,
-            errback=self.psNodeAffiliationsGetEb,
-        )
+    async def start(self):
+        try:
+            affiliations = await self.host.bridge.psNodeAffiliationsGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get node affiliations: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(affiliations)
+            self.host.quit()
 
 
 class NodeAffiliationsSet(base.CommandBase):
@@ -445,7 +403,6 @@
             use_verbose=True,
             help=_("set affiliations (for node owner)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         # XXX: we use optional argument syntax for a required one because list of list of 2 elements
@@ -461,26 +418,21 @@
             help=_("entity/affiliation couple(s)"),
         )
 
-    def psNodeAffiliationsSetCb(self):
-        self.disp(_("affiliations have been set"), 1)
-        self.host.quit()
-
-    def psNodeAffiliationsSetEb(self, failure_):
-        self.disp(
-            "can't set node affiliations: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
+    async def start(self):
         affiliations = dict(self.args.affiliations)
-        self.host.bridge.psNodeAffiliationsSet(
-            self.args.service,
-            self.args.node,
-            affiliations,
-            self.profile,
-            callback=self.psNodeAffiliationsSetCb,
-            errback=self.psNodeAffiliationsSetEb,
-        )
+        try:
+            await self.host.bridge.psNodeAffiliationsSet(
+                self.args.service,
+                self.args.node,
+                affiliations,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set node affiliations: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("affiliations have been set"), 1)
+            self.host.quit()
 
 
 class NodeAffiliations(base.CommandBase):
@@ -506,29 +458,23 @@
             pubsub_flags={C.NODE},
             help=_("retrieve node subscriptions (for node owner)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psNodeSubscriptionsGetCb(self, subscriptions):
-        self.output(subscriptions)
-        self.host.quit()
-
-    def psNodeSubscriptionsGetEb(self, failure_):
-        self.disp(
-            "can't get node subscriptions: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.psNodeSubscriptionsGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psNodeSubscriptionsGetCb,
-            errback=self.psNodeSubscriptionsGetEb,
-        )
+    async def start(self):
+        try:
+            subscriptions = await self.host.bridge.psNodeSubscriptionsGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get node subscriptions: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(subscriptions)
+            self.host.quit()
 
 
 class StoreSubscriptionAction(argparse.Action):
@@ -566,7 +512,6 @@
             use_verbose=True,
             help=_("set/modify subscriptions (for node owner)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         # XXX: we use optional argument syntax for a required one because list of list of 2 elements
@@ -583,25 +528,20 @@
             help=_("entity/subscription couple(s)"),
         )
 
-    def psNodeSubscriptionsSetCb(self):
-        self.disp(_("subscriptions have been set"), 1)
-        self.host.quit()
-
-    def psNodeSubscriptionsSetEb(self, failure_):
-        self.disp(
-            "can't set node subscriptions: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.psNodeSubscriptionsSet(
-            self.args.service,
-            self.args.node,
-            self.args.subscriptions,
-            self.profile,
-            callback=self.psNodeSubscriptionsSetCb,
-            errback=self.psNodeSubscriptionsSetEb,
-        )
+    async def start(self):
+        try:
+            self.host.bridge.psNodeSubscriptionsSet(
+                self.args.service,
+                self.args.node,
+                self.args.subscriptions,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set node subscriptions: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("subscriptions have been set"), 1)
+            self.host.quit()
 
 
 class NodeSubscriptions(base.CommandBase):
@@ -627,28 +567,24 @@
             use_verbose=True,
             help=_("set/replace a schema"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument("schema", help=_("schema to set (must be XML)"))
 
-    def psSchemaSetCb(self):
-        self.disp(_("schema has been set"), 1)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.psSchemaSet(
-            self.args.service,
-            self.args.node,
-            self.args.schema,
-            self.profile,
-            callback=self.psSchemaSetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't set schema: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            await self.host.bridge.psSchemaSet(
+                self.args.service,
+                self.args.node,
+                self.args.schema,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set schema: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("schema has been set"), 1)
+            self.host.quit()
 
 
 class NodeSchemaEdit(base.CommandBase, common.BaseEdit):
@@ -666,30 +602,26 @@
             help=_("edit a schema"),
         )
         common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR)
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psSchemaSetCb(self):
-        self.disp(_("schema has been set"), 1)
-        self.host.quit()
+    async def publish(self, schema):
+        try:
+            await self.host.bridge.psSchemaSet(
+                self.args.service,
+                self.args.node,
+                schema,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't set schema: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("schema has been set"), 1)
+            self.host.quit()
 
-    def publish(self, schema):
-        self.host.bridge.psSchemaSet(
-            self.args.service,
-            self.args.node,
-            schema,
-            self.profile,
-            callback=self.psSchemaSetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't set schema: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
-    def psSchemaGetCb(self, schema):
+    async def psSchemaGetCb(self, schema):
         try:
             from lxml import etree
         except ImportError:
@@ -707,20 +639,20 @@
                 etree.tostring(schema_elt, encoding="utf-8", pretty_print=True)
             )
             content_file_obj.seek(0)
-        self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj)
+        await self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj)
 
-    def start(self):
-        self.host.bridge.psSchemaGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psSchemaGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't edit schema: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            schema = await self.host.bridge.psSchemaGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't edit schema: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.psSchemaGetCb(schema)
 
 
 class NodeSchemaGet(base.CommandBase):
@@ -735,30 +667,27 @@
             use_verbose=True,
             help=_("get schema"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psSchemaGetCb(self, schema):
-        if not schema:
-            self.disp(_("no schema found"), 1)
-            self.host.quit(1)
-        self.output(schema)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.psSchemaGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psSchemaGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get schema: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            schema = await self.host.bridge.psSchemaGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get schema: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            if schema:
+                await self.output(schema)
+                self.host.quit()
+            else:
+                self.disp(_("no schema found"), 1)
+                self.host.quit(1)
 
 
 class NodeSchema(base.CommandBase):
@@ -799,7 +728,6 @@
             pubsub_flags={C.NODE},
             help=_("publish a new item or update an existing one"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -809,32 +737,29 @@
             help=_("id, URL of the item to update, keyword, or nothing for new item"),
         )
 
-    def psItemsSendCb(self, published_id):
-        if published_id:
-            self.disp("Item published at {pub_id}".format(pub_id=published_id))
-        else:
-            self.disp("Item published")
-        self.host.quit(C.EXIT_OK)
-
-    def start(self):
+    async def start(self):
         element, etree = xml_tools.etreeParse(self, sys.stdin)
         element = xml_tools.getPayload(self, element)
         payload = etree.tostring(element, encoding="unicode")
 
-        self.host.bridge.psItemSend(
-            self.args.service,
-            self.args.node,
-            payload,
-            self.args.item,
-            {},
-            self.profile,
-            callback=self.psItemsSendCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't send item: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            published_id = await self.host.bridge.psItemSend(
+                self.args.service,
+                self.args.node,
+                payload,
+                self.args.item,
+                {},
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(_(f"can't send item: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            if published_id:
+                self.disp("Item published at {pub_id}".format(pub_id=published_id))
+            else:
+                self.disp("Item published")
+            self.host.quit(C.EXIT_OK)
 
 
 class Get(base.CommandBase):
@@ -848,7 +773,6 @@
             pubsub_flags={C.NODE, C.MULTI_ITEMS},
             help=_("get pubsub item(s)"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -860,26 +784,23 @@
         #  TODO: a key(s) argument to select keys to display
         # TODO: add MAM filters
 
-    def psItemsGetCb(self, ps_result):
-        self.output(ps_result[0])
-        self.host.quit(C.EXIT_OK)
-
-    def psItemsGetEb(self, failure_):
-        self.disp("can't get pubsub items: {reason}".format(reason=failure_), error=True)
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.psItemsGet(
-            self.args.service,
-            self.args.node,
-            self.args.max,
-            self.args.items,
-            self.args.sub_id,
-            self.getPubsubExtra(),
-            self.profile,
-            callback=self.psItemsGetCb,
-            errback=self.psItemsGetEb,
-        )
+    async def start(self):
+        try:
+            ps_result = await self.host.bridge.psItemsGet(
+                self.args.service,
+                self.args.node,
+                self.args.max,
+                self.args.items,
+                self.args.sub_id,
+                self.getPubsubExtra(),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get pubsub items: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(ps_result[0])
+            self.host.quit(C.EXIT_OK)
 
 
 class Delete(base.CommandBase):
@@ -892,7 +813,6 @@
             pubsub_flags={C.NODE, C.ITEM, C.SINGLE_ITEM},
             help=_("delete an item"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -902,31 +822,28 @@
             "-N", "--notify", action="store_true", help=_("notify deletion")
         )
 
-    def psItemsDeleteCb(self):
-        self.disp(_("item {item_id} has been deleted").format(item_id=self.args.item))
-        self.host.quit(C.EXIT_OK)
-
-    def start(self):
+    async def start(self):
         if not self.args.item:
             self.parser.error(_("You need to specify an item to delete"))
         if not self.args.force:
             message = _("Are you sure to delete item {item_id} ?").format(
                 item_id=self.args.item
             )
-            self.host.confirmOrQuit(message, _("item deletion cancelled"))
-        self.host.bridge.psRetractItem(
-            self.args.service,
-            self.args.node,
-            self.args.item,
-            self.args.notify,
-            self.profile,
-            callback=self.psItemsDeleteCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't delete item: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+            await self.host.confirmOrQuit(message, _("item deletion cancelled"))
+        try:
+            await self.host.bridge.psRetractItem(
+                self.args.service,
+                self.args.node,
+                self.args.item,
+                self.args.notify,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(_(f"can't delete item: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f"item {self.args.item} has been deleted"))
+            self.host.quit(C.EXIT_OK)
 
 
 class Edit(base.CommandBase, common.BaseEdit):
@@ -946,12 +863,8 @@
     def add_parser_options(self):
         pass
 
-    def edit(self, content_file_path, content_file_obj):
-        # we launch editor
-        self.runEditor("pubsub_editor_args", content_file_path, content_file_obj)
-
-    def publish(self, content):
-        published_id = self.host.bridge.psItemSend(
+    async def publish(self, content):
+        published_id = await self.host.bridge.psItemSend(
             self.pubsub_service,
             self.pubsub_node,
             content,
@@ -964,7 +877,7 @@
         else:
             self.disp("Item published")
 
-    def getItemData(self, service, node, item):
+    async def getItemData(self, service, node, item):
         try:
             from lxml import etree
         except ImportError:
@@ -974,10 +887,10 @@
             )
             self.host.quit(1)
         items = [item] if item else []
-        item_raw = self.host.bridge.psItemsGet(
+        item_raw = (await self.host.bridge.psItemsGet(
             service, node, 1, items, "", {}, self.profile
-        )[0][0]
-        parser = etree.XMLParser(remove_blank_text=True)
+        ))[0][0]
+        parser = etree.XMLParser(remove_blank_text=True, recover=True)
         item_elt = etree.fromstring(item_raw, parser)
         item_id = item_elt.get("id")
         try:
@@ -987,11 +900,14 @@
             return ""
         return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id
 
-    def start(self):
-        self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj = (
-            self.getItemPath()
-        )
-        self.edit(content_file_path, content_file_obj)
+    async def start(self):
+        (self.pubsub_service,
+         self.pubsub_node,
+         self.pubsub_item,
+         content_file_path,
+         content_file_obj) = await self.getItemPath()
+        await self.runEditor("pubsub_editor_args", content_file_path, content_file_obj)
+        self.host.quit()
 
 
 class Subscribe(base.CommandBase):
@@ -1005,34 +921,30 @@
             use_verbose=True,
             help=_("subscribe to a node"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psSubscribeCb(self, sub_id):
-        self.disp(_("subscription done"), 1)
-        if sub_id:
-            self.disp(_("subscription id: {sub_id}").format(sub_id=sub_id))
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.psSubscribe(
-            self.args.service,
-            self.args.node,
-            {},
-            self.profile,
-            callback=self.psSubscribeCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't subscribe to node: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            sub_id = await self.host.bridge.psSubscribe(
+                self.args.service,
+                self.args.node,
+                {},
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(_(f"can't subscribe to node: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("subscription done"), 1)
+            if sub_id:
+                self.disp(_("subscription id: {sub_id}").format(sub_id=sub_id))
+            self.host.quit()
 
 
 class Unsubscribe(base.CommandBase):
-    # TODO: voir pourquoi NodeNotFound sur subscribe juste après unsubscribe
+    # FIXME: check why we get a a NodeNotFound on subscribe just after unsubscribe
 
     def __init__(self, host):
         base.CommandBase.__init__(
@@ -1044,27 +956,23 @@
             use_verbose=True,
             help=_("unsubscribe from a node"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psUnsubscribeCb(self):
-        self.disp(_("subscription removed"), 1)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.psUnsubscribe(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psUnsubscribeCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't unsubscribe from node: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            await self.host.bridge.psUnsubscribe(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(_(f"can't unsubscribe from node: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("subscription removed"), 1)
+            self.host.quit()
 
 
 class Subscriptions(base.CommandBase):
@@ -1077,27 +985,23 @@
             use_pubsub=True,
             help=_("retrieve all subscriptions on a service"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psSubscriptionsGetCb(self, subscriptions):
-        self.output(subscriptions)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.psSubscriptionsGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psSubscriptionsGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't retrieve subscriptions: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            subscriptions = await self.host.bridge.psSubscriptionsGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(_(f"can't retrieve subscriptions: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(subscriptions)
+            self.host.quit()
 
 
 class Affiliations(base.CommandBase):
@@ -1110,33 +1014,29 @@
             use_pubsub=True,
             help=_("retrieve all affiliations on a service"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psAffiliationsGetCb(self, affiliations):
-        self.output(affiliations)
-        self.host.quit()
-
-    def psAffiliationsGetEb(self, failure_):
-        self.disp(
-            "can't get node affiliations: {reason}".format(reason=failure_), error=True
-        )
-        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-
-    def start(self):
-        self.host.bridge.psAffiliationsGet(
-            self.args.service,
-            self.args.node,
-            self.profile,
-            callback=self.psAffiliationsGetCb,
-            errback=self.psAffiliationsGetEb,
-        )
+    async def start(self):
+        try:
+            affiliations = await self.host.bridge.psAffiliationsGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(
+                f"can't get node affiliations: {e}", error=True
+            )
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(affiliations)
+            self.host.quit()
 
 
 class Search(base.CommandBase):
-    """this command do a search without using MAM
+    """This command do a search without using MAM
 
     This commands checks every items it finds by itself,
     so it may be heavy in resources both for server and client
@@ -1159,7 +1059,6 @@
             use_verbose=True,
             help=_("search items corresponding to filters"),
         )
-        self.need_loop = True
 
     @property
     def etree(self):
@@ -1317,30 +1216,26 @@
         )
         self.parser.add_argument("command", nargs=argparse.REMAINDER)
 
-    def psItemsGetEb(self, failure_, service, node):
-        self.disp(
-            "can't get pubsub items at {service} (node: {node}): {reason}".format(
-                service=service, node=node, reason=failure_
-            ),
-            error=True,
-        )
-        self.to_get -= 1
-
-    def getItems(self, depth, service, node, items):
-        search = partial(self.search, depth=depth)
-        errback = partial(self.psItemsGetEb, service=service, node=node)
-        self.host.bridge.psItemsGet(
-            service,
-            node,
-            self.args.node_max,
-            items,
-            "",
-            self.getPubsubExtra(),
-            self.profile,
-            callback=search,
-            errback=errback,
-        )
+    async def getItems(self, depth, service, node, items):
         self.to_get += 1
+        try:
+            items_data = await self.host.bridge.psItemsGet(
+                service,
+                node,
+                self.args.node_max,
+                items,
+                "",
+                self.getPubsubExtra(),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(
+                f"can't get pubsub items at {service} (node: {node}): {e}",
+                error=True,
+            )
+            self.to_get -= 1
+        else:
+            await self.search(items_data, depth)
 
     def _checkPubsubURL(self, match, found_nodes):
         """check that the matched URL is an xmpp: one
@@ -1360,13 +1255,13 @@
                     found_node["item"] = url_data["item"]
                 found_nodes.append(found_node)
 
-    def getSubNodes(self, item, depth):
+    async def getSubNodes(self, item, depth):
         """look for pubsub URIs in item, and getItems on the linked nodes"""
         found_nodes = []
         checkURI = partial(self._checkPubsubURL, found_nodes=found_nodes)
         strings.RE_URL.sub(checkURI, item)
         for data in found_nodes:
-            self.getItems(
+            await self.getItems(
                 depth + 1,
                 data["service"],
                 data["node"],
@@ -1452,7 +1347,11 @@
             elif type_ == "python":
                 if item_xml is None:
                     item_xml = self.parseXml(item)
-                cmd_ns = {"item": item, "item_xml": item_xml}
+                cmd_ns = {
+                    "etree": self.etree,
+                    "item": item,
+                    "item_xml": item_xml
+                    }
                 try:
                     keep = eval(value, cmd_ns)
                 except SyntaxError as e:
@@ -1483,7 +1382,7 @@
 
         return True, item
 
-    def doItemAction(self, item, metadata):
+    async def doItemAction(self, item, metadata):
         """called when item has been kepts and the action need to be done
 
         @param item(unicode): accepted item
@@ -1491,7 +1390,7 @@
         action = self.args.action
         if action == "print" or self.host.verbosity > 0:
             try:
-                self.output(item)
+                await self.output(item)
             except self.etree.XMLSyntaxError:
                 # item is not valid XML, but a string
                 # can happen when --only-matching is used
@@ -1521,22 +1420,22 @@
                 2,
             )
             if action == "exec":
-                ret = subprocess.call(cmd_args)
+                p = await asyncio.create_subprocess_exec(*cmd_args)
+                ret = await p.wait()
             else:
-                p = subprocess.Popen(cmd_args, stdin=subprocess.PIPE)
-                p.communicate(item.encode("utf-8"))
-                ret = p.wait()
+                p = await asyncio.create_subprocess_exec(*cmd_args,
+                                                         stdin=subprocess.PIPE)
+                await p.communicate(item.encode(sys.getfilesystemencoding()))
+                ret = p.returncode
             if ret != 0:
                 self.disp(
                     A.color(
                         C.A_FAILURE,
-                        _("executed command failed with exit code {code}").format(
-                            code=ret
-                        ),
+                        _(f"executed command failed with exit code {ret}"),
                     )
                 )
 
-    def search(self, items_data, depth):
+    async def search(self, items_data, depth):
         """callback of getItems
 
         this method filters items, get sub nodes if needed,
@@ -1548,11 +1447,11 @@
         items, metadata = items_data
         for item in items:
             if depth < self.args.max_depth:
-                self.getSubNodes(item, depth)
+                await self.getSubNodes(item, depth)
             keep, item = self.filter(item)
             if not keep:
                 continue
-            self.doItemAction(item, metadata)
+            await self.doItemAction(item, metadata)
 
         #  we check if we got all getItems results
         self.to_get -= 1
@@ -1561,7 +1460,7 @@
             self.host.quit()
         assert self.to_get > 0
 
-    def start(self):
+    async def start(self):
         if self.args.command:
             if self.args.action not in self.EXEC_ACTIONS:
                 self.parser.error(
@@ -1584,7 +1483,7 @@
         self.args.namespace = dict(
             self.args.namespace + [("pubsub", "http://jabber.org/protocol/pubsub")]
         )
-        self.getItems(0, self.args.service, self.args.node, self.args.items)
+        await self.getItems(0, self.args.service, self.args.node, self.args.items)
 
 
 class Transform(base.CommandBase):
@@ -1597,7 +1496,6 @@
             pubsub_flags={C.NODE, C.MULTI_ITEMS},
             help=_("modify items of a node using an external command/script"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -1630,18 +1528,18 @@
                    'Return "DELETE" string to delete the item, and "SKIP" to ignore it'),
         )
 
-    def psItemsSendCb(self, item_ids, metadata):
+    async def psItemsSendCb(self, item_ids, metadata):
         if item_ids:
             self.disp(_('items published with ids {item_ids}').format(
                 item_ids=', '.join(item_ids)))
         else:
             self.disp(_('items published'))
         if self.args.all:
-            return self.handleNextPage(metadata)
+            return await self.handleNextPage(metadata)
         else:
             self.host.quit()
 
-    def handleNextPage(self, metadata):
+    async def handleNextPage(self, metadata):
         """Retrieve new page through RSM or quit if we're in the last page
 
         use to handle --all option
@@ -1672,24 +1570,27 @@
 
         extra = self.getPubsubExtra()
         extra['rsm_after'] = last
-        self.host.bridge.psItemsGet(
-            self.args.service,
-            self.args.node,
-            self.args.rsm_max,
-            self.args.items,
-            "",
-            extra,
-            self.profile,
-            callback=self.psItemsGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't retrieve items: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            ps_result = await self.host.bridge.psItemsGet(
+                self.args.service,
+                self.args.node,
+                self.args.rsm_max,
+                self.args.items,
+                "",
+                extra,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(
+                f"can't retrieve items: {e}", error=True
+            )
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.psItemsGetCb(ps_result)
 
-    def psItemsGetCb(self, ps_result):
+    async def psItemsGetCb(self, ps_result):
         items, metadata = ps_result
+        encoding = 'utf-8'
         new_items = []
 
         for item in items:
@@ -1707,48 +1608,46 @@
 
             # we launch the command to filter the item
             try:
-                p = subprocess.Popen(self.args.command_path, stdin=subprocess.PIPE,
-                                                             stdout=subprocess.PIPE)
+                p = await asyncio.create_subprocess_exec(
+                    self.args.command_path,
+                    stdin=subprocess.PIPE,
+                    stdout=subprocess.PIPE)
             except OSError as e:
                 exit_code = C.EXIT_CMD_NOT_FOUND if e.errno == 2 else C.EXIT_ERROR
-                e = str(e).decode('utf-8', errors="ignore")
-                self.disp("Can't execute the command: {msg}".format(msg=e), error=True)
+                self.disp(f"Can't execute the command: {e}", error=True)
                 self.host.quit(exit_code)
-            cmd_std_out, cmd_std_err = p.communicate(item.encode("utf-8"))
-            ret = p.wait()
+            encoding = "utf-8"
+            cmd_std_out, cmd_std_err = await p.communicate(item.encode(encoding))
+            ret = p.returncode
             if ret != 0:
-                self.disp("The command returned a non zero status while parsing the "
-                          "following item:\n\n{item}".format(item=item), error=True)
+                self.disp(f"The command returned a non zero status while parsing the "
+                          f"following item:\n\n{item}", error=True)
                 if self.args.ignore_errors:
                     continue
                 else:
                     self.host.quit(C.EXIT_CMD_ERROR)
             if cmd_std_err is not None:
-                cmd_std_err = cmd_std_err.decode('utf-8', errors='ignore')
+                cmd_std_err = cmd_std_err.decode(encoding, errors='ignore')
                 self.disp(cmd_std_err, error=True)
-            cmd_std_out = cmd_std_out.strip()
+            cmd_std_out = cmd_std_out.decode(encoding).strip()
             if cmd_std_out == "DELETE":
                 item_elt, __ = xml_tools.etreeParse(self, item)
                 item_id = item_elt.get('id')
-                self.disp(_("Deleting item {item_id}").format(item_id=item_id))
+                self.disp(_(f"Deleting item {item_id}"))
                 if self.args.apply:
-                    # FIXME: we don't wait for item to be retracted which can cause
-                    #        trouble in case of error just before the end of the command
-                    #        (the error message may be missed).
-                    #        Once moved to Python 3, we must wait for it by using a
-                    #        coroutine.
-                    self.host.bridge.psRetractItem(
-                        self.args.service,
-                        self.args.node,
-                        item_id,
-                        False,
-                        self.profile,
-                        errback=partial(
-                            self.errback,
-                            msg=_("can't delete item [%s]: {}" % item_id),
-                            exit_code=C.EXIT_BRIDGE_ERRBACK,
-                        ),
-                    )
+                    try:
+                        await self.host.bridge.psRetractItem(
+                            self.args.service,
+                            self.args.node,
+                            item_id,
+                            False,
+                            self.profile,
+                        )
+                    except Exception as e:
+                        self.disp(
+                            f"can't delete item {item_id}: {e}", error=True
+                        )
+                        self.host.quit(C.EXIT_BRIDGE_ERRBACK)
                 continue
             elif cmd_std_out == "SKIP":
                 item_elt, __ = xml_tools.etreeParse(self, item)
@@ -1774,39 +1673,29 @@
         if not self.args.apply:
             # on dry run we have nothing to wait for, we can quit
             if self.args.all:
-                return self.handleNextPage(metadata)
+                return await self.handleNextPage(metadata)
             self.host.quit()
         else:
             if self.args.admin:
-                self.host.bridge.psAdminItemsSend(
+                bridge_method = self.host.bridge.psAdminItemsSend
+            else:
+                bridge_method = self.host.bridge.psItemsSend
+
+            try:
+                ps_result = await bridge_method(
                     self.args.service,
                     self.args.node,
                     new_items,
                     "",
                     self.profile,
-                    callback=partial(self.psItemsSendCb, metadata=metadata),
-                    errback=partial(
-                        self.errback,
-                        msg=_("can't send item: {}"),
-                        exit_code=C.EXIT_BRIDGE_ERRBACK,
-                    ),
                 )
+            except Exception as e:
+                self.disp(f"can't send item: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
             else:
-                self.host.bridge.psItemsSend(
-                    self.args.service,
-                    self.args.node,
-                    new_items,
-                    "",
-                    self.profile,
-                    callback=partial(self.psItemsSendCb, metadata=metadata),
-                    errback=partial(
-                        self.errback,
-                        msg=_("can't send item: {}"),
-                        exit_code=C.EXIT_BRIDGE_ERRBACK,
-                    ),
-                )
+                await self.psItemsSendCb(ps_result, metadata=metadata)
 
-    def start(self):
+    async def start(self):
         if self.args.all and self.args.order_by != C.ORDER_BY_CREATION:
             self.check_duplicates = True
             self.items_ids = []
@@ -1819,21 +1708,22 @@
                 "but this method is not safe, and some items may be missed.\n---\n"))
         else:
             self.check_duplicates = False
-        self.host.bridge.psItemsGet(
-            self.args.service,
-            self.args.node,
-            self.args.max,
-            self.args.items,
-            "",
-            self.getPubsubExtra(),
-            self.profile,
-            callback=self.psItemsGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't retrieve items: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+
+        try:
+            ps_result = await self.host.bridge.psItemsGet(
+                self.args.service,
+                self.args.node,
+                self.args.max,
+                self.args.items,
+                "",
+                self.getPubsubExtra(),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't retrieve items: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.psItemsGetCb(ps_result)
 
 
 class Uri(base.CommandBase):
@@ -1847,7 +1737,6 @@
             pubsub_flags={C.NODE, C.SINGLE_ITEM},
             help=_("build URI"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -1871,19 +1760,19 @@
         self.disp(uri.buildXMPPUri("pubsub", **uri_args))
         self.host.quit()
 
-    def start(self):
+    async def start(self):
         if not self.args.service:
-            self.host.bridge.asyncGetParamA(
-                "JabberID",
-                "Connection",
-                profile_key=self.args.profile,
-                callback=self.display_uri,
-                errback=partial(
-                    self.errback,
-                    msg=_("can't retrieve jid: {}"),
-                    exit_code=C.EXIT_BRIDGE_ERRBACK,
-                ),
-            )
+            try:
+                jid_ = await self.host.bridge.asyncGetParamA(
+                    "JabberID",
+                    "Connection",
+                    profile_key=self.args.profile
+                )
+            except Exception as e:
+                self.disp(f"can't retrieve jid: {e}", error=True)
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+            else:
+                self.display_uri(jid_)
         else:
             self.display_uri(None)
 
@@ -1898,7 +1787,6 @@
             pubsub_flags={C.NODE},
             help=_("create a Pubsub hook"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -1928,22 +1816,22 @@
                     _("{path} is not a file").format(path=self.args.hook_arg)
                 )
 
-    def start(self):
+    async def start(self):
         self.checkArgs(self)
-        self.host.bridge.psHookAdd(
-            self.args.service,
-            self.args.node,
-            self.args.type,
-            self.args.hook_arg,
-            self.args.persistent,
-            self.profile,
-            callback=self.host.quit,
-            errback=partial(
-                self.errback,
-                msg=_("can't create hook: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.psHookAdd(
+                self.args.service,
+                self.args.node,
+                self.args.type,
+                self.args.hook_arg,
+                self.args.persistent,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't create hook: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.host.quit()
 
 
 class HookDelete(base.CommandBase):
@@ -1956,7 +1844,6 @@
             pubsub_flags={C.NODE},
             help=_("delete a Pubsub hook"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -1976,27 +1863,22 @@
             ),
         )
 
-    def psHookRemoveCb(self, nb_deleted):
-        self.disp(
-            _("{nb_deleted} hook(s) have been deleted").format(nb_deleted=nb_deleted)
-        )
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         HookCreate.checkArgs(self)
-        self.host.bridge.psHookRemove(
-            self.args.service,
-            self.args.node,
-            self.args.type,
-            self.args.hook_arg,
-            self.profile,
-            callback=self.psHookRemoveCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't delete hook: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            nb_deleted = await self.host.bridge.psHookRemove(
+                self.args.service,
+                self.args.node,
+                self.args.type,
+                self.args.hook_arg,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't delete hook: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f"{nb_deleted} hook(s) have been deleted"))
+            self.host.quit()
 
 
 class HookList(base.CommandBase):
@@ -2008,27 +1890,23 @@
             use_output=C.OUTPUT_LIST_DICT,
             help=_("list hooks of a profile"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def psHookListCb(self, data):
-        if not data:
-            self.disp(_("No hook found."))
-        self.output(data)
-        self.host.quit()
-
-    def start(self):
-        self.host.bridge.psHookList(
-            self.profile,
-            callback=self.psHookListCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't list hooks: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+    async def start(self):
+        try:
+            data = await self.host.bridge.psHookList(
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't list hooks: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            if not data:
+                self.disp(_("No hook found."))
+            await self.output(data)
+            self.host.quit()
 
 
 class Hook(base.CommandBase):
--- a/sat_frontends/jp/cmd_roster.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_roster.py	Wed Sep 25 08:56:41 2019 +0200
@@ -20,77 +20,67 @@
 
 from . import base
 from collections import OrderedDict
-from functools import partial
 from sat.core.i18n import _
 from sat_frontends.jp.constants import Const as C
-from twisted.words.protocols.jabber import jid
+from sat_frontends.tools import jid
+from sat.tools.common.ansi import ANSI as A
 
 __commands__ = ["Roster"]
 
 
-
-class Purge(base.CommandBase):
+class Get(base.CommandBase):
 
     def __init__(self, host):
-        super(Purge, self).__init__(host, 'purge', help=_('Purge the roster from its contacts with no subscription'))
-        self.need_loop = True
+        super().__init__(
+            host, 'get', use_output=C.OUTPUT_DICT, use_verbose=True,
+            extra_outputs = {"default": self.default_output},
+            help=_('retrieve the roster entities'))
 
     def add_parser_options(self):
-        self.parser.add_argument("--no_from", action="store_true", help=_("Also purge contacts with no 'from' subscription"))
-        self.parser.add_argument("--no_to", action="store_true", help=_("Also purge contacts with no 'to' subscription"))
-
-    def start(self):
-        self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error)
+        pass
 
-    def error(self, failure):
-        print((_("Error while retrieving the contacts [%s]") % failure))
-        self.host.quit(1)
+    def default_output(self, data):
+        for contact_jid, contact_data in data.items():
+            all_keys = list(contact_data.keys())
+            keys_to_show = []
+            name = contact_data.get('name', contact_jid.node)
 
-    def ask_confirmation(self, no_sub, no_from, no_to):
-        """Ask the confirmation before removing contacts.
+            if self.verbosity >= 1:
+                keys_to_show.append('groups')
+                all_keys.remove('groups')
+            if self.verbosity >= 2:
+                keys_to_show.extend(all_keys)
 
-        @param no_sub (list[unicode]): list of contacts with no subscription
-        @param no_from (list[unicode]): list of contacts with no 'from' subscription
-        @param no_to (list[unicode]): list of contacts with no 'to' subscription
-        @return bool
-        """
-        if no_sub:
-            print("There's no subscription between profile [%s] and the following contacts:" % self.host.profile)
-            print("    " + "\n    ".join(no_sub))
-        if no_from:
-            print("There's no 'from' subscription between profile [%s] and the following contacts:" % self.host.profile)
-            print("    " + "\n    ".join(no_from))
-        if no_to:
-            print("There's no 'to' subscription between profile [%s] and the following contacts:" % self.host.profile)
-            print("    " + "\n    ".join(no_to))
-        message = "REMOVE them from profile [%s]'s roster" % self.host.profile
-        while True:
-            res = input("%s (y/N)? " % message)
-            if not res or res.lower() == 'n':
-                return False
-            if res.lower() == 'y':
-                return True
+            if name is None:
+                self.disp(A.color(C.A_HEADER, contact_jid))
+            else:
+                self.disp(A.color(C.A_HEADER, name, A.RESET, f" ({contact_jid})"))
+            for k in keys_to_show:
+                value = contact_data[k]
+                if value:
+                    if isinstance(value, list):
+                        value = ', '.join(value)
+                    self.disp(A.color(
+                        "    ", C.A_SUBHEADER, f"{k}: ", A.RESET, str(value)))
 
-    def gotContacts(self, contacts):
-        """Process the list of contacts.
+    async def start(self):
+        try:
+            contacts = await self.host.bridge.getContacts(profile_key=self.host.profile)
+        except Exception as e:
+            self.disp(f"error while retrieving the contacts: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-        @param contacts(list[tuple]): list of contacts with their attributes and groups
-        """
-        no_sub, no_from, no_to = [], [], []
-        for contact, attrs, groups in contacts:
-            from_, to = C.bool(attrs["from"]), C.bool(attrs["to"])
-            if not from_:
-                if not to:
-                    no_sub.append(contact)
-                elif self.args.no_from:
-                    no_from.append(contact)
-            elif not to and self.args.no_to:
-                no_to.append(contact)
-        if not no_sub and not no_from and not no_to:
-            print("Nothing to do - there's a from and/or to subscription(s) between profile [%s] and each of its contacts" % self.host.profile)
-        elif self.ask_confirmation(no_sub, no_from, no_to):
-            for contact in no_sub + no_from + no_to:
-                self.host.bridge.delContact(contact, profile_key=self.host.profile, callback=lambda __: None, errback=lambda failure: None)
+        contacts_dict = {}
+        for contact_jid_s, data, groups in contacts:
+            # FIXME: we have to convert string to bool here for historical reason
+            #        getContacts format should be changed and serialised properly
+            for key in ('from', 'to', 'ask'):
+                if key in data:
+                    data[key] = C.bool(data[key])
+            data['groups'] = list(groups)
+            contacts_dict[jid.JID(contact_jid_s)] = data
+
+        await self.output(contacts_dict)
         self.host.quit()
 
 
@@ -98,23 +88,17 @@
 
     def __init__(self, host):
         super(Stats, self).__init__(host, 'stats', help=_('Show statistics about a roster'))
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def start(self):
-        self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error)
+    async def start(self):
+        try:
+            contacts = await self.host.bridge.getContacts(profile_key=self.host.profile)
+        except Exception as e:
+            self.disp(f"error while retrieving the contacts: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def error(self, failure):
-        print((_("Error while retrieving the contacts [%s]") % failure))
-        self.host.quit(1)
-
-    def gotContacts(self, contacts):
-        """Process the list of contacts.
-
-        @param contacts(list[tuple]): list of contacts with their attributes and groups
-        """
         hosts = {}
         unique_groups = set()
         no_sub, no_from, no_to, no_group, total_group_subscription = 0, 0, 0, 0, 0
@@ -127,7 +111,9 @@
                     no_from += 1
             elif not to:
                 no_to += 1
-            host = jid.JID(contact).host
+
+            host = jid.JID(contact).domain
+
             hosts.setdefault(host, 0)
             hosts[host] += 1
             if groups:
@@ -142,7 +128,8 @@
         print("Number of different hosts: %d" % len(hosts))
         print()
         for host, count in hosts.items():
-            print("Contacts on {host}: {count} ({rate:.1f}%)".format(host=host, count=count, rate=100 * float(count) / len(contacts)))
+            print("Contacts on {host}: {count} ({rate:.1f}%)".format(
+                host=host, count=count, rate=100 * float(count) / len(contacts)))
         print()
         print("Contacts with no 'from' subscription: %d" % no_from)
         print("Contacts with no 'to' subscription: %d" % no_to)
@@ -158,53 +145,90 @@
             groups_per_contact = float(total_group_subscription) / len(contacts)
         except ZeroDivisionError:
             groups_per_contact = 0
-        print("Average groups' subscriptions per contact: {:.1f}".format(groups_per_contact))
+        print(f"Average groups' subscriptions per contact: {groups_per_contact:.1f}")
         print("Contacts not assigned to any group: %d" % no_group)
         self.host.quit()
 
 
-class Get(base.CommandBase):
+class Purge(base.CommandBase):
 
     def __init__(self, host):
-        super(Get, self).__init__(host, 'get', help=_('Retrieve the roster contacts'))
-        self.need_loop = True
+        super(Purge, self).__init__(
+            host, 'purge',
+            help=_('purge the roster from its contacts with no subscription'))
 
     def add_parser_options(self):
-        self.parser.add_argument("--subscriptions", action="store_true", help=_("Show the contacts' subscriptions"))
-        self.parser.add_argument("--groups", action="store_true", help=_("Show the contacts' groups"))
-        self.parser.add_argument("--name", action="store_true", help=_("Show the contacts' names"))
+        self.parser.add_argument(
+            "--no_from", action="store_true",
+            help=_("also purge contacts with no 'from' subscription"))
+        self.parser.add_argument(
+            "--no_to", action="store_true",
+            help=_("also purge contacts with no 'to' subscription"))
 
-    def start(self):
-        self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error)
+    async def start(self):
+        try:
+            contacts = await self.host.bridge.getContacts(self.host.profile)
+        except Exception as e:
+            self.disp(f"error while retrieving the contacts: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def error(self, failure):
-        print((_("Error while retrieving the contacts [%s]") % failure))
-        self.host.quit(1)
+        no_sub, no_from, no_to = [], [], []
+        for contact, attrs, groups in contacts:
+            from_, to = C.bool(attrs["from"]), C.bool(attrs["to"])
+            if not from_:
+                if not to:
+                    no_sub.append(contact)
+                elif self.args.no_from:
+                    no_from.append(contact)
+            elif not to and self.args.no_to:
+                no_to.append(contact)
+        if not no_sub and not no_from and not no_to:
+            self.disp(
+                f"Nothing to do - there's a from and/or to subscription(s) between "
+                f"profile {self.host.profile!r} and each of its contacts"
+            )
+        elif await self.ask_confirmation(no_sub, no_from, no_to):
+            for contact in no_sub + no_from + no_to:
+                try:
+                    await self.host.bridge.delContact(
+                        contact, profile_key=self.host.profile)
+                except Exception as e:
+                    self.disp(f"can't delete contact {contact!r}: {e}", error=True)
+                else:
+                    self.disp(f"contact {contact!r} has been removed")
 
-    def gotContacts(self, contacts):
-        """Process the list of contacts.
+        self.host.quit()
+
+    async def ask_confirmation(self, no_sub, no_from, no_to):
+        """Ask the confirmation before removing contacts.
 
-        @param contacts(list[tuple]): list of contacts with their attributes and groups
+        @param no_sub (list[unicode]): list of contacts with no subscription
+        @param no_from (list[unicode]): list of contacts with no 'from' subscription
+        @param no_to (list[unicode]): list of contacts with no 'to' subscription
+        @return bool
         """
-        field_count = 1  # only display the contact by default
-        if self.args.subscriptions:
-            field_count += 3  # ask, from, to
-        if self.args.name:
-            field_count += 1
-        if self.args.groups:
-            field_count += 1
-        for contact, attrs, groups in contacts:
-            args = [contact]
-            if self.args.subscriptions:
-                args.append("ask" if C.bool(attrs["ask"]) else "")
-                args.append("from" if C.bool(attrs["from"]) else "")
-                args.append("to" if C.bool(attrs["to"]) else "")
-            if self.args.name:
-                args.append(str(attrs.get("name", "")))
-            if self.args.groups:
-                args.append("\t".join(groups) if groups else "")
-            print(";".join(["{}"] * field_count).format(*args).encode("utf-8"))
-        self.host.quit()
+        if no_sub:
+            self.disp(
+                f"There's no subscription between profile {self.host.profile!r} and the "
+                f"following contacts:")
+            self.disp("    " + "\n    ".join(no_sub))
+        if no_from:
+            self.disp(
+                f"There's no 'from' subscription between profile {self.host.profile!r} "
+                f"and the following contacts:")
+            self.disp("    " + "\n    ".join(no_from))
+        if no_to:
+            self.disp(
+                f"There's no 'to' subscription between profile {self.host.profile!r} and "
+                f"the following contacts:")
+            self.disp("    " + "\n    ".join(no_to))
+        message = f"REMOVE them from profile {self.host.profile}'s roster"
+        while True:
+            res = await self.host.ainput(f"{message} (y/N)? ")
+            if not res or res.lower() == 'n':
+                return False
+            if res.lower() == 'y':
+                return True
 
 
 class Resync(base.CommandBase):
@@ -212,27 +236,24 @@
     def __init__(self, host):
         super(Resync, self).__init__(
             host, 'resync', help=_('do a full resynchronisation of roster with server'))
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def rosterResyncCb(self):
-        self.disp(_("Roster resynchronized"))
-        self.host.quit(C.EXIT_OK)
-
-    def start(self):
-        self.host.bridge.rosterResync(profile_key=self.host.profile,
-                                      callback=self.rosterResyncCb,
-                                      errback=partial(
-                                          self.errback,
-                                          msg=_("can't resynchronise roster: {}"),
-                                          exit_code=C.EXIT_BRIDGE_ERRBACK,
-                                      ))
+    async def start(self):
+        try:
+            await self.host.bridge.rosterResync(profile_key=self.host.profile)
+        except Exception as e:
+            self.disp(f"can't resynchronise roster: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_("Roster resynchronized"))
+            self.host.quit(C.EXIT_OK)
 
 
 class Roster(base.CommandBase):
     subcommands = (Get, Stats, Purge, Resync)
 
     def __init__(self, host):
-        super(Roster, self).__init__(host, 'roster', use_profile=True, help=_("Manage an entity's roster"))
+        super(Roster, self).__init__(
+            host, 'roster', use_profile=True, help=_("Manage an entity's roster"))
--- a/sat_frontends/jp/cmd_shell.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_shell.py	Wed Sep 25 08:56:41 2019 +0200
@@ -18,16 +18,16 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-from . import base
 import cmd
 import sys
+import shlex
+import subprocess
+from . import base
 from sat.core.i18n import _
 from sat.core import exceptions
 from sat_frontends.jp.constants import Const as C
 from sat_frontends.jp import arg_tools
 from sat.tools.common.ansi import ANSI as A
-import shlex
-import subprocess
 
 __commands__ = ["Shell"]
 INTRO = _(
@@ -44,7 +44,8 @@
 class Shell(base.CommandBase, cmd.Cmd):
     def __init__(self, host):
         base.CommandBase.__init__(
-            self, host, "shell", help=_("launch jp in shell (REPL) mode")
+            self, host, "shell",
+            help=_("launch jp in shell (REPL) mode")
         )
         cmd.Cmd.__init__(self)
 
@@ -152,13 +153,15 @@
             help_list = self._cur_parser.format_help().split("\n\n")
             print(("\n\n".join(help_list[1 if self.path else 2 :])))
 
-    def do_debug(self, args):
-        """launch internal debugger"""
-        try:
-            import ipdb as pdb
-        except ImportError:
-            import pdb
-        pdb.set_trace()
+    # FIXME: debug crashes on exit and is not that useful,
+    #        keeping it until refactoring, may be removed entirely then
+    # def do_debug(self, args):
+    #     """launch internal debugger"""
+    #     try:
+    #         import ipdb as pdb
+    #     except ImportError:
+    #         import pdb
+    #     pdb.set_trace()
 
     def do_verbose(self, args):
         """show verbose mode, or (de)activate it"""
@@ -188,10 +191,7 @@
 
     def do_version(self, args):
         """show current SàT/jp version"""
-        try:
-            self.host.run(["--version"])
-        except SystemExit:
-            pass
+        self.run_cmd(['--version'])
 
     def do_shell(self, args):
         """launch an external command (you can use ![command] too)"""
@@ -292,7 +292,9 @@
         """alias for quit"""
         self.do_quit(args)
 
-    def start(self):
+    async def start(self):
+        # FIXME: "shell" is currently kept synchronous as it works well as it
+        #        and it will be refactored soon.
         default_profile = self.host.bridge.profileNameGet(C.PROF_KEY_DEFAULT)
         self._not_default_profile = self.profile != default_profile
         self.path = []
@@ -300,4 +302,4 @@
         self.use = {}
         self.verbose = False
         self.update_path()
-        self.cmdloop(INTRO.encode("utf-8"))
+        self.cmdloop(INTRO)
--- a/sat_frontends/jp/cmd_ticket.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_ticket.py	Wed Sep 25 08:56:41 2019 +0200
@@ -22,7 +22,6 @@
 from sat.core.i18n import _
 from sat_frontends.jp import common
 from sat_frontends.jp.constants import Const as C
-from functools import partial
 import json
 import os
 
@@ -44,37 +43,31 @@
             use_output=C.OUTPUT_LIST_XMLUI,
             help=_("get tickets"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         pass
 
-    def ticketsGetCb(self, tickets_data):
-        self.output(tickets_data[0])
-        self.host.quit(C.EXIT_OK)
-
-    def getTickets(self):
-        self.host.bridge.ticketsGet(
-            self.args.service,
-            self.args.node,
-            self.args.max,
-            self.args.items,
-            "",
-            self.getPubsubExtra(),
-            self.profile,
-            callback=self.ticketsGetCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't get tickets: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
-
-    def start(self):
-        common.URIFinder(self, os.getcwd(), "tickets", self.getTickets, meta_map={})
+    async def start(self):
+        await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={})
+        try:
+            tickets_data = await self.host.bridge.ticketsGet(
+                self.args.service,
+                self.args.node,
+                self.args.max,
+                self.args.items,
+                "",
+                self.getPubsubExtra(),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't get tickets: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.output(tickets_data[0])
+            self.host.quit(C.EXIT_OK)
 
 
-class Import(base.CommandAnswering):
+class Import(base.CommandBase):
     # TODO: factorize with blog/import
 
     def __init__(self, host):
@@ -82,9 +75,9 @@
             host,
             "import",
             use_progress=True,
+            use_verbose=True,
             help=_("import tickets from external software/dataset"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -109,7 +102,8 @@
             default=[],
             metavar=("IMPORTED_FIELD", "DEST_FIELD"),
             help=_(
-                "specified field in import data will be put in dest field (default: use same field name, or ignore if it doesn't exist)"
+                "specified field in import data will be put in dest field (default: use "
+                "same field name, or ignore if it doesn't exist)"
             ),
         )
         self.parser.add_argument(
@@ -125,97 +119,92 @@
             default="",
             metavar="PUBSUB_NODE",
             help=_(
-                "PubSub node where the items must be uploaded (default: tickets' defaults)"
+                "PubSub node where the items must be uploaded (default: tickets' "
+                "defaults)"
             ),
         )
         self.parser.add_argument(
             "location",
             nargs="?",
             help=_(
-                "importer data location (see importer description), nothing to show importer description"
+                "importer data location (see importer description), nothing to show "
+                "importer description"
             ),
         )
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("Tickets upload started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("Tickets uploaded successfully"), 2)
 
-    def onProgressError(self, error_msg):
-        self.disp(_("Error while uploading tickets: {}").format(error_msg), error=True)
+    async def onProgressError(self, error_msg):
+        self.disp(_(f"Error while uploading tickets: {error_msg}"), error=True)
 
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to upload tickets: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def start(self):
+    async def start(self):
         if self.args.location is None:
+            # no location, the list of importer or description is requested
             for name in ("option", "service", "node"):
                 if getattr(self.args, name):
                     self.parser.error(
-                        _(
-                            "{name} argument can't be used without location argument"
-                        ).format(name=name)
-                    )
+                        _(f"{name} argument can't be used without location argument"))
             if self.args.importer is None:
                 self.disp(
                     "\n".join(
                         [
-                            "{}: {}".format(name, desc)
-                            for name, desc in self.host.bridge.ticketsImportList()
+                            f"{name}: {desc}"
+                            for name, desc in await self.host.bridge.ticketsImportList()
                         ]
                     )
                 )
             else:
                 try:
-                    short_desc, long_desc = self.host.bridge.ticketsImportDesc(
+                    short_desc, long_desc = await self.host.bridge.ticketsImportDesc(
                         self.args.importer
                     )
                 except Exception as e:
-                    msg = [l for l in str(e).split("\n") if l][
-                        -1
-                    ]  # we only keep the last line
-                    self.disp(msg)
-                    self.host.quit(1)
+                    self.disp(f"can't get importer description: {e}", error=True)
+                    self.host.quit(C.EXIT_BRIDGE_ERRBACK)
                 else:
-                    self.disp(
-                        "{name}: {short_desc}\n\n{long_desc}".format(
-                            name=self.args.importer,
-                            short_desc=short_desc,
-                            long_desc=long_desc,
-                        )
-                    )
+                    self.disp(f"{name}: {short_desc}\n\n{long_desc}")
             self.host.quit()
         else:
             # we have a location, an import is requested
+
+            if self.args.progress:
+                # we use a custom progress bar template as we want a counter
+                self.pbar_template = [
+                    _("Progress: "), ["Percentage"], " ", ["Bar"], " ",
+                    ["Counter"], " ", ["ETA"]
+                ]
+
             options = {key: value for key, value in self.args.option}
             fields_map = dict(self.args.map)
             if fields_map:
                 if FIELDS_MAP in options:
                     self.parser.error(
-                        _(
-                            "fields_map must be specified either preencoded in --option or using --map, but not both at the same time"
-                        )
+                        _("fields_map must be specified either preencoded in --option or "
+                          "using --map, but not both at the same time")
                     )
                 options[FIELDS_MAP] = json.dumps(fields_map)
 
-            def gotId(id_):
-                self.progress_id = id_
-
-            self.host.bridge.ticketsImport(
-                self.args.importer,
-                self.args.location,
-                options,
-                self.args.service,
-                self.args.node,
-                self.profile,
-                callback=gotId,
-                errback=self.error,
-            )
+            try:
+                progress_id = await self.host.bridge.ticketsImport(
+                    self.args.importer,
+                    self.args.location,
+                    options,
+                    self.args.service,
+                    self.args.node,
+                    self.profile,
+                )
+            except Exception as e:
+                self.disp(
+                    _(f"Error while trying to import tickets: {e}"),
+                    error=True,
+                )
+                self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+            else:
+                await self.set_progress_id(progress_id)
 
 
 class Ticket(base.CommandBase):
--- a/sat_frontends/jp/cmd_uri.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_uri.py	Wed Sep 25 08:56:41 2019 +0200
@@ -42,8 +42,9 @@
             "uri", help=_("XMPP URI to parse")
         )
 
-    def start(self):
-        self.output(uri.parseXMPPUri(self.args.uri))
+    async def start(self):
+        await self.output(uri.parseXMPPUri(self.args.uri))
+        self.host.quit()
 
 
 class Build(base.CommandBase):
@@ -65,9 +66,10 @@
             help=_("URI fields"),
         )
 
-    def start(self):
+    async def start(self):
         fields = dict(self.args.fields) if self.args.fields else {}
         self.disp(uri.buildXMPPUri(self.args.type, path=self.args.path, **fields))
+        self.host.quit()
 
 
 class Uri(base.CommandBase):
--- a/sat_frontends/jp/common.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/common.py	Wed Sep 25 08:56:41 2019 +0200
@@ -17,6 +17,14 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import json
+import os
+import os.path
+import time
+import tempfile
+import asyncio
+import shlex
+from pathlib import Path
 from sat_frontends.jp.constants import Const as C
 from sat.core.i18n import _
 from sat.core import exceptions
@@ -26,15 +34,6 @@
 from sat.tools import config
 from configparser import NoSectionError, NoOptionError
 from collections import namedtuple
-from functools import partial
-import json
-import os
-import os.path
-import time
-import tempfile
-import subprocess
-import glob
-import shlex
 
 # defaut arguments used for some known editors (editing with metadata)
 VIM_SPLIT_ARGS = "-c 'set nospr|vsplit|wincmd w|next|wincmd w'"
@@ -76,18 +75,18 @@
     """Return directory used to store temporary files
 
     @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
-    @param cat_dir(unicode): directory of the category (e.g. "blog")
+    @param cat_dir(str): directory of the category (e.g. "blog")
     @param sub_dir(str): sub directory where data need to be put
         profile can be used here, or special directory name
         sub_dir will be escaped to be usable in path (use regex.pathUnescape to find
         initial str)
-    @return (str): path to the dir
+    @return (Path): path to the dir
     """
     local_dir = config.getConfig(sat_conf, "", "local_dir", Exception)
-    path = [local_dir.encode("utf-8"), cat_dir.encode("utf-8")]
+    path_elts = [local_dir, cat_dir]
     if sub_dir is not None:
-        path.append(regex.pathEscape(sub_dir))
-    return os.path.join(*path)
+        path_elts.append(regex.pathEscape(sub_dir))
+    return Path(*path_elts)
 
 
 def parse_args(host, cmd_line, **format_kw):
@@ -121,12 +120,14 @@
         @param cat_dir(unicode): directory to use for drafts
             this will be a sub-directory of SàT's local_dir
         @param use_metadata(bool): True is edition need a second file for metadata
-            most of signature change with use_metadata with an additional metadata argument.
-            This is done to raise error if a command needs metadata but forget the flag, and vice versa
+            most of signature change with use_metadata with an additional metadata
+            argument.
+            This is done to raise error if a command needs metadata but forget the flag,
+            and vice versa
         """
         self.host = host
         self.sat_conf = config.parseMainConf()
-        self.cat_dir_str = cat_dir.encode("utf-8")
+        self.cat_dir = cat_dir
         self.use_metadata = use_metadata
 
     def secureUnlink(self, path):
@@ -135,21 +136,21 @@
         This method is used to prevent accidental deletion of a draft
         If there are more file in SECURE_UNLINK_DIR than SECURE_UNLINK_MAX,
         older file are deleted
-        @param path(str): file to unlink
+        @param path(Path, str): file to unlink
         """
-        if not os.path.isfile(path):
+        path = Path(path).resolve()
+        if not path.is_file:
             raise OSError("path must link to a regular file")
-        if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)):
+        if path.parent != getTmpDir(self.sat_conf, self.cat_dir):
             self.disp(
-                "File {} is not in SàT temporary hierarchy, we do not remove it".format(
-                    path
-                ),
+                f"File {path} is not in SàT temporary hierarchy, we do not remove "
+                f"it",
                 2,
             )
             return
         # we have 2 files per draft with use_metadata, so we double max
         unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX
-        backup_dir = getTmpDir(self.sat_conf, self.cat_dir_str, SECURE_UNLINK_DIR)
+        backup_dir = getTmpDir(self.sat_conf, self.cat_dir, SECURE_UNLINK_DIR)
         if not os.path.exists(backup_dir):
             os.makedirs(backup_dir)
         filename = os.path.basename(path)
@@ -170,21 +171,15 @@
                 self.host.disp("Purging backup file {}".format(path), 2)
                 os.unlink(path)
 
-    def runEditor(
-        self,
-        editor_args_opt,
-        content_file_path,
-        content_file_obj,
-        meta_file_path=None,
-        meta_ori=None,
-    ):
-        """run editor to edit content and metadata
+    async def runEditor(self, editor_args_opt, content_file_path, content_file_obj,
+                        meta_file_path=None, meta_ori=None):
+        """Run editor to edit content and metadata
 
         @param editor_args_opt(unicode): option in [jp] section in configuration for
             specific args
         @param content_file_path(str): path to the content file
         @param content_file_obj(file): opened file instance
-        @param meta_file_path(str, None): metadata file path
+        @param meta_file_path(str, Path, None): metadata file path
             if None metadata will not be used
         @param meta_ori(dict, None): original cotent of metadata
             can't be used if use_metadata is False
@@ -223,26 +218,27 @@
             args = [content_file_path]
 
         # actual editing
-        editor_exit = subprocess.call([editor] + args)
+        editor_process = await asyncio.create_subprocess_exec(
+            editor, *[str(a) for a in args])
+        editor_exit = await editor_process.wait()
 
         # edition will now be checked, and data will be sent if it was a success
         if editor_exit != 0:
             self.disp(
-                "Editor exited with an error code, so temporary file has not be deleted, and item is not published.\nYou can find temporary file at {path}".format(
-                    path=content_file_path
-                ),
+                f"Editor exited with an error code, so temporary file has not be "
+                f"deleted, and item is not published.\nYou can find temporary file "
+                f"at {content_file_path}",
                 error=True,
             )
         else:
             # main content
             try:
-                with open(content_file_path, "rb") as f:
+                with content_file_path.open("rb") as f:
                     content = f.read()
             except (OSError, IOError):
                 self.disp(
-                    "Can read file at {content_path}, have it been deleted?\nCancelling edition".format(
-                        content_path=content_file_path
-                    ),
+                    f"Can read file at {content_file_path}, have it been deleted?\n"
+                    f"Cancelling edition",
                     error=True,
                 )
                 self.host.quit(C.EXIT_NOT_FOUND)
@@ -250,50 +246,46 @@
             # metadata
             if self.use_metadata:
                 try:
-                    with open(meta_file_path, "rb") as f:
+                    with meta_file_path.open("rb") as f:
                         metadata = json.load(f)
                 except (OSError, IOError):
                     self.disp(
-                        "Can read file at {meta_file_path}, have it been deleted?\nCancelling edition".format(
-                            content_path=content_file_path, meta_path=meta_file_path
-                        ),
+                        f"Can read file at {meta_file_path}, have it been deleted?\n"
+                        f"Cancelling edition",
                         error=True,
                     )
                     self.host.quit(C.EXIT_NOT_FOUND)
                 except ValueError:
                     self.disp(
-                        "Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n"
-                        + "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format(
-                            content_path=content_file_path, meta_path=meta_file_path
-                        ),
+                        f"Can't parse metadata, please check it is correct JSON format. "
+                        f"Cancelling edition.\nYou can find tmp file at "
+                        f"{content_file_path} and temporary meta file at "
+                        f"{meta_file_path}.",
                         error=True,
                     )
                     self.host.quit(C.EXIT_DATA_ERROR)
 
             if self.use_metadata and not metadata.get("publish", True):
                 self.disp(
-                    'Publication blocked by "publish" key in metadata, cancelling edition.\n\n'
-                    + "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format(
-                        content_path=content_file_path, meta_path=meta_file_path
-                    ),
+                    f'Publication blocked by "publish" key in metadata, cancelling '
+                    f'edition.\n\ntemporary file path:\t{content_file_path}\nmetadata '
+                    f'file path:\t{meta_file_path}',
                     error=True,
                 )
                 self.host.quit()
 
             if len(content) == 0:
                 self.disp("Content is empty, cancelling the edition")
-                if not content_file_path.startswith(
-                    getTmpDir(self.sat_conf, self.cat_dir_str)
-                ):
+                if content_file_path.parent != getTmpDir(self.sat_conf, self.cat_dir):
                     self.disp(
                         "File are not in SàT temporary hierarchy, we do not remove them",
                         2,
                     )
                     self.host.quit()
-                self.disp("Deletion of {}".format(content_file_path), 2)
+                self.disp(f"Deletion of {content_file_path}", 2)
                 os.unlink(content_file_path)
                 if self.use_metadata:
-                    self.disp("Deletion of {}".format(meta_file_path), 2)
+                    self.disp(f"Deletion of {meta_file_path}".format(meta_file_path), 2)
                     os.unlink(meta_file_path)
                 self.host.quit()
 
@@ -309,24 +301,21 @@
                 content = content.decode("utf-8-sig")  # we use utf-8-sig to avoid BOM
                 try:
                     if self.use_metadata:
-                        self.publish(content, metadata)
+                        await self.publish(content, metadata)
                     else:
-                        self.publish(content)
+                        await self.publish(content)
                 except Exception as e:
                     if self.use_metadata:
                         self.disp(
-                            "Error while sending your item, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format(
-                                content_path=content_file_path,
-                                meta_path=meta_file_path,
-                                reason=e,
-                            ),
+                            f"Error while sending your item, the temporary files have "
+                            f"been kept at {content_file_path} and {meta_file_path}: "
+                            f"{e}",
                             error=True,
                         )
                     else:
                         self.disp(
-                            "Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format(
-                                content_path=content_file_path, reason=e
-                            ),
+                            f"Error while sending your item, the temporary file has been "
+                            f"kept at {content_file_path}: {e}",
                             error=True,
                         )
                     self.host.quit(1)
@@ -335,41 +324,38 @@
             if self.use_metadata:
                 self.secureUnlink(meta_file_path)
 
-    def publish(self, content):
+    async def publish(self, content):
         # if metadata is needed, publish will be called with it last argument
         raise NotImplementedError
 
     def getTmpFile(self):
         """Create a temporary file
 
-        @param suff (str): suffix to use for the filename
-        @return (tuple(file, str)): opened (w+b) file object and file path
+        @return (tuple(file, Path)): opened (w+b) file object and file path
         """
         suff = "." + self.getTmpSuff()
-        cat_dir_str = self.cat_dir_str
-        tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, self.profile.encode("utf-8"))
-        if not os.path.exists(tmp_dir):
+        cat_dir_str = self.cat_dir
+        tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, self.profile)
+        if not tmp_dir.exists():
             try:
-                os.makedirs(tmp_dir)
+                tmp_dir.mkdir(parents=True)
             except OSError as e:
                 self.disp(
-                    "Can't create {path} directory: {reason}".format(
-                        path=tmp_dir, reason=e
-                    ),
+                    f"Can't create {tmp_dir} directory: {e}",
                     error=True,
                 )
                 self.host.quit(1)
         try:
             fd, path = tempfile.mkstemp(
-                suffix=suff.encode("utf-8"),
+                suffix=suff,
                 prefix=time.strftime(cat_dir_str + "_%Y-%m-%d_%H:%M:%S_"),
                 dir=tmp_dir,
                 text=True,
             )
-            return os.fdopen(fd, "w+b"), path
+            return os.fdopen(fd, "w+b"), Path(path)
         except OSError as e:
             self.disp(
-                "Can't create temporary file: {reason}".format(reason=e), error=True
+                f"Can't create temporary file: {e}", error=True
             )
             self.host.quit(1)
 
@@ -377,27 +363,26 @@
         """Get most recently edited file
 
         @param profile(unicode): profile linked to the draft
-        @return(str): full path of current file
+        @return(Path): full path of current file
         """
         # we guess the item currently edited by choosing
         # the most recent file corresponding to temp file pattern
         # in tmp_dir, excluding metadata files
-        cat_dir_str = self.cat_dir_str
-        tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, profile.encode("utf-8"))
+        tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, profile)
         available = [
-            path
-            for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + "_*"))
-            if not path.endswith(METADATA_SUFF)
+            p
+            for p in tmp_dir.glob(f'{self.cat_dir}_*')
+            if not p.match(f"*{METADATA_SUFF}")
         ]
         if not available:
             self.disp(
-                "Could not find any content draft in {path}".format(path=tmp_dir),
+                f"Could not find any content draft in {tmp_dir}",
                 error=True,
             )
             self.host.quit(1)
-        return max(available, key=lambda path: os.stat(path).st_mtime)
+        return max(available, key=lambda p: p.stat().st_mtime)
 
-    def getItemData(self, service, node, item):
+    async def getItemData(self, service, node, item):
         """return formatted content, metadata (or not if use_metadata is false), and item id"""
         raise NotImplementedError
 
@@ -405,8 +390,8 @@
         """return suffix used for content file"""
         return "xml"
 
-    def getItemPath(self):
-        """retrieve item path (i.e. service and node) from item argument
+    async def getItemPath(self):
+        """Retrieve item path (i.e. service and node) from item argument
 
         This method is obviously only useful for edition of PubSub based features
         """
@@ -419,15 +404,15 @@
             # user wants to continue current draft
             content_file_path = self.getCurrentFile(self.profile)
             self.disp("Continuing edition of current draft", 2)
-            content_file_obj = open(content_file_path, "r+b")
+            content_file_obj = content_file_path.open("r+b")
             # we seek at the end of file in case of an item already exist
             # this will write content of the existing item at the end of the draft.
             # This way no data should be lost.
             content_file_obj.seek(0, os.SEEK_END)
         elif self.args.draft_path:
             # there is an existing draft that we use
-            content_file_path = os.path.expanduser(self.args.draft_path)
-            content_file_obj = open(content_file_path, "r+b")
+            content_file_path = self.args.draft_path.expanduser()
+            content_file_obj = content_file_path.open("r+b")
             # we seek at the end for the same reason as above
             content_file_obj.seek(0, os.SEEK_END)
         else:
@@ -438,9 +423,9 @@
             self.disp("Editing requested published item", 2)
             try:
                 if self.use_metadata:
-                    content, metadata, item = self.getItemData(service, node, item)
+                    content, metadata, item = await self.getItemData(service, node, item)
                 else:
-                    content, item = self.getItemData(service, node, item)
+                    content, item = await self.getItemData(service, node, item)
             except Exception as e:
                 # FIXME: ugly but we have not good may to check errors in bridge
                 if "item-not-found" in str(e):
@@ -451,13 +436,14 @@
                     else:
                         self.disp(
                             _(
-                                'item "{item_id}" not found, we create a new item with this id'
-                            ).format(item_id=item),
+                                f'item "{item}" not found, we create a new item with'
+                                f'this id'
+                            ),
                             2,
                         )
                     content_file_obj.seek(0)
                 else:
-                    self.disp("Error while retrieving item: {}".format(e))
+                    self.disp(f"Error while retrieving item: {e}")
                     self.host.quit(C.EXIT_ERROR)
             else:
                 # item exists, we write content
@@ -468,7 +454,7 @@
                 content_file_obj.write(content.encode("utf-8"))
                 content_file_obj.seek(0)
                 self.disp(
-                    _('item "{item_id}" found, we edit it').format(item_id=item), 2
+                    _(f'item "{item}" found, we edit it'), 2
                 )
         else:
             self.disp("Editing a new item", 2)
@@ -757,89 +743,70 @@
         return self.display(**kwargs_)
 
 
-class URIFinder(object):
-    """Helper class to find URIs in well-known locations"""
+async def fill_well_known_uri(command, path, key, meta_map=None):
+    """Look for URIs in well-known location and fill appropriate args if suitable
+
+    @param command(CommandBase): command instance
+        args of this instance will be updated with found values
+    @param path(unicode): absolute path to use as a starting point to look for URIs
+    @param key(unicode): key to look for
+    @param meta_map(dict, None): if not None, map metadata to arg name
+        key is metadata used attribute name
+        value is name to actually use, or None to ignore
+        use empty dict to only retrieve URI
+        possible keys are currently:
+            - labels
+    """
+    args = command.args
+    if args.service or args.node:
+        # we only look for URIs if a service and a node are not already specified
+        return
+
+    host = command.host
 
-    def __init__(self, command, path, key, callback, meta_map=None):
-        """
-        @param command(CommandBase): command instance
-            args of this instance will be updated with found values
-        @param path(unicode): absolute path to use as a starting point to look for URIs
-        @param key(unicode): key to look for
-        @param callback(callable): method to call once URIs are found (or not)
-        @param meta_map(dict, None): if not None, map metadata to arg name
-            key is metadata used attribute name
-            value is name to actually use, or None to ignore
-            use empty dict to only retrieve URI
-            possible keys are currently:
-                - labels
-        """
-        if not command.args.service and not command.args.node:
-            self.host = command.host
-            self.args = command.args
-            self.key = key
-            self.callback = callback
-            self.meta_map = meta_map
-            self.host.bridge.URIFind(
-                path,
-                [key],
-                callback=self.URIFindCb,
-                errback=partial(
-                    command.errback,
-                    msg=_("can't find " + key + " URI: {}"),
-                    exit_code=C.EXIT_BRIDGE_ERRBACK,
-                ),
-            )
-        else:
-            callback()
+    try:
+        uris_data = await host.bridge.URIFind(path, [key])
+    except Exception as e:
+        host.disp(f"can't find {key} URI: {e}", error=True)
+        host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def setMetadataList(self, uri_data, key):
-        """Helper method to set list of values from metadata
+    try:
+        uri_data = uris_data[key]
+    except KeyError:
+        host.disp(
+            _(f"No {key} URI specified for this project, please specify service and "
+              f"node"),
+            error=True,
+        )
+        host.quit(C.EXIT_NOT_FOUND)
 
-        @param uri_data(dict): data of the found URI
-        @param key(unicode): key of the value to retrieve
-        """
-        new_values_json = uri_data.get(key)
+    uri = uri_data["uri"]
+
+    # set extra metadata if they are specified
+    for data_key in ['labels']:
+        new_values_json = uri_data.get(data_key)
         if uri_data is not None:
-            if self.meta_map is None:
-                dest = key
+            if meta_map is None:
+                dest = data_key
             else:
-                dest = self.meta_map.get(key)
+                dest = meta_map.get(data_key)
                 if dest is None:
-                    return
+                    continue
 
             try:
-                values = getattr(self.args, key)
+                values = getattr(args, data_key)
             except AttributeError:
-                raise exceptions.InternalError(
-                    'there is no "{key}" arguments'.format(key=key)
-                )
+                raise exceptions.InternalError(f'there is no {data_key!r} arguments')
             else:
                 if values is None:
                     values = []
                 values.extend(json.loads(new_values_json))
-                setattr(self.args, dest, values)
+                setattr(args, dest, values)
 
-    def URIFindCb(self, uris_data):
-        try:
-            uri_data = uris_data[self.key]
-        except KeyError:
-            self.host.disp(
-                _(
-                    "No {key} URI specified for this project, please specify service and node"
-                ).format(key=self.key),
-                error=True,
-            )
-            self.host.quit(C.EXIT_NOT_FOUND)
-        else:
-            uri = uri_data["uri"]
-
-        self.setMetadataList(uri_data, "labels")
-        parsed_uri = xmpp_uri.parseXMPPUri(uri)
-        try:
-            self.args.service = parsed_uri["path"]
-            self.args.node = parsed_uri["node"]
-        except KeyError:
-            self.host.disp(_("Invalid URI found: {uri}").format(uri=uri), error=True)
-            self.host.quit(C.EXIT_DATA_ERROR)
-        self.callback()
+    parsed_uri = xmpp_uri.parseXMPPUri(uri)
+    try:
+        args.service = parsed_uri["path"]
+        args.node = parsed_uri["node"]
+    except KeyError:
+        host.disp(_(f"Invalid URI found: {uri}"), error=True)
+        host.quit(C.EXIT_DATA_ERROR)
--- a/sat_frontends/jp/constants.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/constants.py	Wed Sep 25 08:56:41 2019 +0200
@@ -66,6 +66,7 @@
     A_LEVEL_COLORS = (A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN)
     A_SUCCESS = A.BOLD + A.FG_GREEN
     A_FAILURE = A.BOLD + A.FG_RED
+    A_WARNING = A.BOLD + A.FG_RED
     #  A_PROMPT_* is for shell
     A_PROMPT_PATH = A.BOLD + A.FG_CYAN
     A_PROMPT_SUF = A.BOLD
@@ -83,6 +84,7 @@
     EXIT_DATA_ERROR = 17  # data needed for a command is invalid
     EXIT_MISSING_FEATURE = 18  # a needed plugin or feature is not available
     EXIT_USER_CANCELLED = 20  # user cancelled action
+    EXIT_INTERNAL_ERROR = 111  # unexpected error
     EXIT_FILE_NOT_EXE = (
         126
     )  # a file to be executed was found, but it was not an executable utility (cf. man 1 exit)
--- a/sat_frontends/jp/jp	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/jp	Wed Sep 25 08:56:41 2019 +0200
@@ -21,5 +21,4 @@
 
 if __name__ == "__main__":
     jp = base.Jp()
-    jp.import_plugins()
     jp.run()
--- a/sat_frontends/jp/output_xmlui.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/output_xmlui.py	Wed Sep 25 08:56:41 2019 +0200
@@ -39,11 +39,11 @@
             C.OUTPUT_LIST_XMLUI, "simple", self.xmlui_list, default=True
         )
 
-    def xmlui(self, data):
+    async def xmlui(self, data):
         xmlui = xmlui_manager.create(self.host, data)
-        xmlui.show(values_only=True, read_only=True)
+        await xmlui.show(values_only=True, read_only=True)
         self.host.disp("")
 
-    def xmlui_list(self, data):
+    async def xmlui_list(self, data):
         for d in data:
-            self.xmlui(d)
+            await self.xmlui(d)
--- a/sat_frontends/jp/xmlui_manager.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/xmlui_manager.py	Wed Sep 25 08:56:41 2019 +0200
@@ -17,14 +17,14 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+from functools import partial
 from sat.core.log import getLogger
-
-log = getLogger(__name__)
 from sat_frontends.tools import xmlui as xmlui_base
 from sat_frontends.jp.constants import Const as C
 from sat.tools.common.ansi import ANSI as A
 from sat.core.i18n import _
-from functools import partial
+
+log = getLogger(__name__)
 
 # workflow constants
 
@@ -67,7 +67,7 @@
     def name(self):
         return self._xmlui_name
 
-    def show(self):
+    async def show(self):
         """display current widget
 
         must be overriden by subclasses
@@ -178,14 +178,14 @@
     def __init__(self, xmlui_parent):
         Widget.__init__(self, xmlui_parent)
 
-    def show(self):
+    async def show(self):
         self.host.disp('')
 
 
 class TextWidget(xmlui_base.TextWidget, ValueWidget):
     type = "text"
 
-    def show(self):
+    async def show(self):
         self.host.disp(self.value)
 
 
@@ -199,7 +199,7 @@
         except AttributeError:
             return None
 
-    def show(self, no_lf=False, ansi=""):
+    async def show(self, no_lf=False, ansi=""):
         """show label
 
         @param no_lf(bool): same as for [JP.disp]
@@ -214,16 +214,16 @@
 class StringWidget(xmlui_base.StringWidget, InputWidget):
     type = "string"
 
-    def show(self):
+    async def show(self):
         if self.read_only or self.root.read_only:
             self.disp(self.value)
         else:
             elems = []
             self.verboseName(elems)
             if self.value:
-                elems.append(_("(enter: {default})").format(default=self.value))
+                elems.append(_(f"(enter: {self.value})"))
             elems.extend([C.A_HEADER, "> "])
-            value = input(A.color(*elems).encode('utf-8'))
+            value = await self.host.ainput(A.color(*elems))
             if value:
                 #  TODO: empty value should be possible
                 #       an escape key should be used for default instead of enter with empty value
@@ -238,7 +238,7 @@
     type = "textbox"
     # TODO: use a more advanced input method
 
-    def show(self):
+    async def show(self):
         self.verboseName()
         if self.read_only or self.root.read_only:
             self.disp(self.value)
@@ -251,9 +251,9 @@
             while True:
                 try:
                     if not values:
-                        line = input(A.color(C.A_HEADER, "[Ctrl-D to finish]> "))
+                        line = await self.host.ainput(A.color(C.A_HEADER, "[Ctrl-D to finish]> "))
                     else:
-                        line = input()
+                        line = await self.host.ainput()
                     values.append(line)
                 except EOFError:
                    break
@@ -264,20 +264,20 @@
 class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget):
     type = "xhtmlbox"
 
-    def show(self):
+    async def show(self):
         # FIXME: we use bridge in a blocking way as permitted by python-dbus
         #        this only for now to make it simpler, it must be refactored
         #        to use async when jp will be fully async (expected for 0.8)
-        self.value = self.host.bridge.syntaxConvert(
+        self.value = await self.host.bridge.syntaxConvert(
             self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile)
-        super(XHTMLBoxWidget, self).show()
+        await super(XHTMLBoxWidget, self).show()
 
 
 class ListWidget(xmlui_base.ListWidget, OptionsWidget):
     type = "list"
     # TODO: handle flags, notably multi
 
-    def show(self):
+    async def show(self):
         if self.root.values_only:
             for value in self.values:
                 self.disp(self.value)
@@ -308,8 +308,8 @@
         choice = None
         limit_max = len(self.options) - 1
         while choice is None or choice < 0 or choice > limit_max:
-            choice = input(
-                A.color(C.A_HEADER, _("your choice (0-{max}): ").format(max=limit_max))
+            choice = await self.host.ainput(
+                A.color(C.A_HEADER, _(f"your choice (0-{limit_max}): "))
             )
             try:
                 choice = int(choice)
@@ -322,7 +322,7 @@
 class BoolWidget(xmlui_base.BoolWidget, InputWidget):
     type = "bool"
 
-    def show(self):
+    async def show(self):
         disp_true = A.color(A.FG_GREEN, "TRUE")
         disp_false = A.color(A.FG_RED, "FALSE")
         if self.read_only or self.root.read_only:
@@ -338,7 +338,7 @@
             while choice not in ("0", "1"):
                 elems = [C.A_HEADER, _("your choice (0,1): ")]
                 self.verboseName(elems)
-                choice = input(A.color(*elems))
+                choice = await self.host.ainput(A.color(*elems))
             self.value = bool(int(choice))
             self.disp("")
 
@@ -365,9 +365,9 @@
     def _xmluiRemove(self, widget):
         self.children.remove(widget)
 
-    def show(self):
+    async def show(self):
         for child in self.children:
-            child.show()
+            await child.show()
 
 
 class VerticalContainer(xmlui_base.VerticalContainer, Container):
@@ -381,7 +381,7 @@
 class LabelContainer(xmlui_base.PairsContainer, Container):
     type = "label"
 
-    def show(self):
+    async def show(self):
         for child in self.children:
             no_lf = False
             # we check linked widget type
@@ -399,9 +399,9 @@
                         no_lf = True
                     elif wid_type == "bool" and for_widget.read_only:
                         no_lf = True
-                child.show(no_lf=no_lf, ansi=A.FG_CYAN)
+                await child.show(no_lf=no_lf, ansi=A.FG_CYAN)
             else:
-                child.show()
+                await child.show()
 
 
 ## Dialogs ##
@@ -415,24 +415,61 @@
     def disp(self, *args, **kwargs):
         self.host.disp(*args, **kwargs)
 
-    def show(self):
+    async def show(self):
         """display current dialog
 
         must be overriden by subclasses
         """
         raise NotImplementedError(self.__class__)
 
+class MessageDialog(xmlui_base.MessageDialog, Dialog):
+
+    def __init__(self, xmlui_parent, title, message, level):
+        Dialog.__init__(self, xmlui_parent)
+        xmlui_base.MessageDialog.__init__(self, xmlui_parent)
+        self.title, self.message, self.level = title, message, level
+
+    async def show(self):
+        # TODO: handle level
+        if self.title:
+            self.disp(A.color(C.A_HEADER, self.title))
+        self.disp(self.message)
+
 
 class NoteDialog(xmlui_base.NoteDialog, Dialog):
-    def show(self):
-        # TODO: handle title and level
-        self.disp(self.message)
 
     def __init__(self, xmlui_parent, title, message, level):
         Dialog.__init__(self, xmlui_parent)
         xmlui_base.NoteDialog.__init__(self, xmlui_parent)
         self.title, self.message, self.level = title, message, level
 
+    async def show(self):
+        # TODO: handle title and level
+        self.disp(self.message)
+
+
+class ConfirmDialog(xmlui_base.ConfirmDialog, Dialog):
+
+    def __init__(self, xmlui_parent, title, message, level, buttons_set):
+        Dialog.__init__(self, xmlui_parent)
+        xmlui_base.ConfirmDialog.__init__(self, xmlui_parent)
+        self.title, self.message, self.level, self.buttons_set = (
+            title, message, level, buttons_set)
+
+    async def show(self):
+        # TODO: handle buttons_set and level
+        self.disp(self.message)
+        if self.title:
+            self.disp(A.color(C.A_HEADER, self.title))
+        input_ = None
+        while input_ not in ('y', 'n'):
+            input_ = await self.host.ainput(f"{self.message} (y/n)? ")
+            input_ = input_.lower()
+        if input_ == 'y':
+            self._xmluiValidated()
+        else:
+            self._xmluiCancelled()
+
 
 ## Factory ##
 
@@ -444,7 +481,7 @@
             return cls
 
 
-class XMLUIPanel(xmlui_base.XMLUIPanel):
+class XMLUIPanel(xmlui_base.AIOXMLUIPanel):
     widget_factory = WidgetFactory()
     _actions = 0  # use to keep track of bridge's launchAction calls
     read_only = False
@@ -470,7 +507,7 @@
     def command(self):
         return self.host.command
 
-    def show(self, workflow=None, read_only=False, values_only=False):
+    async def show(self, workflow=None, read_only=False, values_only=False):
         """display the panel
 
         @param workflow(list, None): command to execute if not None
@@ -489,11 +526,11 @@
         if workflow:
             XMLUIPanel.workflow = workflow
         if XMLUIPanel.workflow:
-            self.runWorkflow()
+            await self.runWorkflow()
         else:
-            self.main_cont.show()
+            await self.main_cont.show()
 
-    def runWorkflow(self):
+    async def runWorkflow(self):
         """loop into workflow commands and execute commands
 
         SUBMIT will interrupt workflow (which will be continue on callback)
@@ -506,7 +543,7 @@
             except IndexError:
                 break
             if cmd == SUBMIT:
-                self.onFormSubmitted()
+                await self.onFormSubmitted()
                 self.submit_id = None  # avoid double submit
                 return
             elif isinstance(cmd, list):
@@ -515,32 +552,32 @@
                 if widget.type == "bool":
                     value = C.bool(value)
                 widget.value = value
-        self.show()
+        await self.show()
 
-    def submitForm(self, callback=None):
+    async def submitForm(self, callback=None):
         XMLUIPanel._submit_cb = callback
-        self.onFormSubmitted()
+        await self.onFormSubmitted()
 
-    def onFormSubmitted(self, ignore=None):
-        #  self.submitted is a Q&D workaround to avoid
+    async def onFormSubmitted(self, ignore=None):
+        # self.submitted is a Q&D workaround to avoid
         # double submit when a workflow is set
         if self.submitted:
             return
         self.submitted = True
-        super(XMLUIPanel, self).onFormSubmitted(ignore)
+        await super(XMLUIPanel, self).onFormSubmitted(ignore)
 
     def _xmluiClose(self):
         pass
 
-    def _launchActionCb(self, data):
+    async def _launchActionCb(self, data):
         XMLUIPanel._actions -= 1
         assert XMLUIPanel._actions >= 0
         if "xmlui" in data:
             xmlui_raw = data["xmlui"]
             xmlui = create(self.host, xmlui_raw)
-            xmlui.show()
+            await xmlui.show()
             if xmlui.submit_id:
-                xmlui.onFormSubmitted()
+                await xmlui.onFormSubmitted()
         # TODO: handle data other than XMLUI
         if not XMLUIPanel._actions:
             if self._submit_cb is None:
@@ -548,19 +585,19 @@
             else:
                 self._submit_cb()
 
-    def _xmluiLaunchAction(self, action_id, data):
+    async def _xmluiLaunchAction(self, action_id, data):
         XMLUIPanel._actions += 1
-        self.host.bridge.launchAction(
-            action_id,
-            data,
-            self.profile,
-            callback=self._launchActionCb,
-            errback=partial(
-                self.command.errback,
-                msg=_("can't launch XMLUI action: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            data = await self.host.bridge.launchAction(
+                action_id,
+                data,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't launch XMLUI action: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self._launchActionCb(data)
 
 
 class XMLUIDialog(xmlui_base.XMLUIDialog):
@@ -568,8 +605,8 @@
     dialog_factory = WidgetFactory()
     read_only = False
 
-    def show(self, __=None):
-        self.dlg.show()
+    async def show(self, __=None):
+        await self.dlg.show()
 
     def _xmluiClose(self):
         pass
--- a/sat_frontends/tools/xmlui.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/tools/xmlui.py	Wed Sep 25 08:56:41 2019 +0200
@@ -925,6 +925,67 @@
         pass
 
 
+class AIOXMLUIPanel(XMLUIPanel):
+    """Asyncio compatible version of XMLUIPanel"""
+
+    async def onFormSubmitted(self, ignore=None):
+        """An XMLUI form has been submited
+
+        call the submit action associated with this form
+        """
+        selected_values = []
+        for ctrl_name in self.ctrl_list:
+            escaped = self.escape(ctrl_name)
+            ctrl = self.ctrl_list[ctrl_name]
+            if isinstance(ctrl["control"], ListWidget):
+                selected_values.append(
+                    (escaped, "\t".join(ctrl["control"]._xmluiGetSelectedValues()))
+                )
+            else:
+                selected_values.append((escaped, ctrl["control"]._xmluiGetValue()))
+        data = dict(selected_values)
+        for key, value in self.hidden.items():
+            data[self.escape(key)] = value
+
+        if self.submit_id is not None:
+            await self.submit(data)
+        else:
+            log.warning(
+                _("The form data is not sent back, the type is not managed properly")
+            )
+            self._xmluiClose()
+
+    async def onFormCancelled(self, *__):
+        """Called when a form is cancelled"""
+        log.debug(_("Cancelling form"))
+        if self.submit_id is not None:
+            data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
+            await self.submit(data)
+        else:
+            log.warning(
+                _("The form data is not sent back, the type is not managed properly")
+            )
+        self._xmluiClose()
+
+    async def submit(self, data):
+        self._xmluiClose()
+        if self.submit_id is None:
+            raise ValueError("Can't submit is self.submit_id is not set")
+        if "session_id" in data:
+            raise ValueError(
+                "session_id must no be used in data, it is automaticaly filled with "
+                "self.session_id if present"
+            )
+        if self.session_id is not None:
+            data["session_id"] = self.session_id
+        await self._xmluiLaunchAction(self.submit_id, data)
+
+    async def _xmluiLaunchAction(self, action_id, data):
+        await self.host.launchAction(
+            action_id, data, callback=self.callback, profile=self.profile
+        )
+
+
 class XMLUIDialog(XMLUIBase):
     dialog_factory = None
 
--- a/setup.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/setup.py	Wed Sep 25 08:56:41 2019 +0200
@@ -87,7 +87,6 @@
     url="https://salut-a-toi.org",
     classifiers=[
         "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Development Status :: 5 - Production/Stable",
         "Environment :: Console",
@@ -110,5 +109,5 @@
     use_scm_version=sat_dev_version if is_dev_version else False,
     install_requires=install_requires,
     package_data={"sat": ["VERSION"]},
-    python_requires=">=3.6",
+    python_requires=">=3.7",
 )
--- a/twisted/plugins/sat_plugin.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/twisted/plugins/sat_plugin.py	Wed Sep 25 08:56:41 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
 # SAT: a jabber client