3 votes

Comment rendre une longue tâche non bloquante avec urwid et asyncio ?

J'écris une application Python curses qui contrôle un processus externe (Linux, si cela peut aider) en envoyant et en recevant des chaînes de caractères par le biais du processus'. stdin y stdout respectivement. L'interface utilise urwid . J'ai écrit une classe pour contrôler le processus externe et quelques autres pour quelques composants urwid.

J'ai également un bouton qui est censé envoyer une commande au processus externe. Cependant, le processus ne répond pas immédiatement et sa tâche prend généralement quelques secondes, pendant lesquelles j'aimerais que l'interface ne se fige pas.

Voici comment je lance le processus enfant :

def run(self, args):
    import io, fcntl, os
    from subprocess import Popen, PIPE

    # Run wpa_cli with arguments, use a thread to feed the process with an input queue
    self._pss = Popen(["wpa_cli"] + args, stdout=PIPE, stdin=PIPE)
    self.stdout = io.TextIOWrapper(self._pss.stdout, encoding="utf-8")
    self.stdin = io.TextIOWrapper(self._pss.stdin, encoding="utf-8", line_buffering=True)

    # Make the process' stdout a non-blocking file
    fd = self.stdout.fileno()
    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
    ...

J'ai dû rendre le flux de sortie du processus non bloquant pour être capable d'analyser sa sortie. Je ne sais pas si c'est important pour ma question.

Voici les méthodes que j'utilise pour contrôler les flux d'entrée et de sortie du processus enfant :

def read(self, parser=None, transform=None, sentinel='>'):
    """ Reads from the controlled process' standard output until a sentinel
    is found. Optionally execute a callable object with every line. Parsed
    lines are placed in a list, which the function returns upon exiting. """

    if not transform:
        transform = lambda str: str

    def readline():
        return transform(self.stdout.readline().strip())

    # Keep a list of (non-empty) parsed lines
    items = []
    for line in iter(readline, sentinel):
        if callable(parser):
            item = parser(line)
            if item is not None:
                items.append(item)
    return items

def send(self, command, echo=True):
    """ Sends a command to the controlled process. Action commands are
    echoed to the standard output. Argument echo controls whether or not
    they're removed by the reader function before parsing. """

    print(command, file=self.stdin)

    # Should we remove the echoed command?
    if not echo:
        self.read(sentinel=command)

Le bouton dont j'ai parlé a juste son callback défini à partir de la fonction d'entrée principale script. Ce callback est censé envoyer une commande au processus enfant et parcourir en boucle les lignes de sortie résultantes jusqu'à ce qu'un texte donné soit trouvé, auquel cas la fonction de callback se termine. Jusque là, le processus produit des informations intéressantes que je dois capturer et afficher dans l'interface utilisateur.

Par exemple :

def button_callback():
    # This is just an illustration

    filter = re.compile('(event1|event2|...)')

    def formatter(text):
        try:
            return re.search(filter, text).group(1)
        except AttributeError:
            return text

    def parser(text):
        if text == 'event1':
            # Update the info Text accordingly
        if text == 'event2':
            # Update the info Text accordingly

    controller.send('command')
    controller.read(sentinel='beacon', parser=parser, transform=formatter)

Ce qu'il faut remarquer, c'est que :

  • le site read() tourne (je n'ai pas trouvé d'autre moyen) même si le flux de sortie du processus est silencieux jusqu'à ce que la valeur sentinelle soit lue à partir des lignes (optionnellement) analysées,
  • l'interface urwid ne se rafraîchira pas avant que la fonction de rappel du bouton ne sorte, ce qui empêche urwid de rafraîchir l'écran.

Je pourrais utiliser un fil mais de ce que j'ai lu urwid soutient asyncio et c'est ce que j'aimerais mettre en place. Vous pouvez me traiter d'idiot car je n'arrive pas à comprendre clairement comment, même après avoir parcouru urwid exemples asyncio et lecture de Python asyncio documentation.

Étant donné qu'il est possible de modifier l'une ou l'autre de ces méthodes, j'aimerais tout de même conserver la classe de contrôle du processus, c'est-à-dire celle qui contient les éléments suivants read() y send() - aussi générique que possible.

Jusqu'à présent, rien de ce que j'ai essayé n'a permis de mettre à jour l'interface alors que le processus était occupé. Le composant qui reçoit les "notifications" du processus est un simple composant de type urwid.Text() widget.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X