Attendre de manière asynchrone la sortie d'un processus comint

12

Tout d'abord, un avertissement. J'ai fait des recherches à ce sujet plusieurs fois, et je suis presque sûr d'avoir déjà trouvé la réponse d'une manière ou d'une autre, mais je ne la comprends tout simplement pas.

Mon problème est le suivant:

  • J'ai un processus en cours d'exécution via comint
  • Je veux envoyer une ligne d'entrée, capturer la sortie et voir quand elle est terminée (lorsque la dernière ligne de la sortie correspond à l'expression rationnelle pour une invite)
  • seulement lorsque le processus a fini d'envoyer la sortie, je veux envoyer une autre ligne d'entrée (par exemple).

Pour un peu de contexte, pensez à un mode majeur implémentant l'interaction avec un programme, qui peut retourner une quantité arbitraire de sortie, dans un temps arbitrairement long. Cela ne devrait pas être une situation inhabituelle, non? D'accord, la partie où je dois attendre entre les entrées est peut-être inhabituelle, mais elle présente quelques avantages par rapport à l'envoi de l'entrée dans son ensemble:

  • le tampon de sortie est bien formaté: entrée sortie entrée sortie ...
  • plus important encore, lorsque vous envoyez beaucoup de texte à un processus, le texte est coupé en morceaux et les morceaux sont collés par le processus; les points de coupure sont arbitraires et cela rend parfois une entrée invalide (mon processus ne recollera pas correctement une entrée coupée au milieu d'un identifiant, par exemple)

Quoi qu'il en soit, inhabituelle ou non, il se trouve qu'il est compliqué. En ce moment, j'utilise quelque chose comme

(defun mymode--wait-for-output ()
  (let ((buffer (mymode-get-buffer)))
    (with-current-buffer buffer
      (goto-char (point-max))
      (forward-line 0)
      (while (not (mymode-looking-at-prompt))
        (accept-process-output nil 0.001)
        (redisplay)
        (goto-char (point-max))
        (forward-line 0))
      (end-of-line))))

et j'appelle cela à chaque fois après avoir envoyé une ligne d'entrée, et avant d'envoyer la suivante. Eh bien ... ça marche, c'est déjà quelque chose.

Mais cela fait aussi bloquer emacs en attendant la sortie. La raison est évidente, et j'ai pensé que si j'incluais une sorte d'asynchrone sleep-for(par exemple 1s) dans la boucle, cela retarderait la sortie de 1s, mais supprimerait le blocage. Sauf qu'il semble que ce type d'asynchrone sleep-for n'existe pas .

Ou alors? Plus généralement, existe-t-il un moyen idiomatique d'y parvenir avec emacs? En d'autres termes:

Comment envoyer une entrée à un processus, attendre la sortie, puis envoyer plus d'entrée, de manière asynchrone?

En cherchant autour (voir les questions connexes), j'ai principalement vu des mentions de sentinelles (mais je ne pense pas que cela s'applique dans mon cas, car le processus ne se termine pas) et de quelques crochets de comint (mais alors quoi? Devrais-je rendre le crochet tampon local, transformer mon "évaluer les lignes restantes" en une fonction, ajouter cette fonction au crochet et nettoyer le crochet par la suite? cela semble vraiment sale, n'est-ce pas?).

Je suis désolé si je ne suis pas clair, ou s'il y a vraiment une réponse évidente disponible quelque part, je suis vraiment confus par toutes les subtilités de l'interaction des processus.

Si nécessaire, je peux faire de tout cela un exemple de travail, mais je crains que cela ne fasse qu'une "question de processus spécifique avec une réponse de processus spécifique" comme toutes celles que j'ai trouvées plus tôt et ne m'a pas aidé.

Quelques questions connexes sur SO:

T. Verron
la source
@nicael Quel est le problème avec les liens associés?
T. Verron
Mais pourquoi avez-vous besoin de les inclure?
nicael
2
Eh bien, j'ai trouvé qu'il s'agissait de questions liées, même si les réponses ne m'ont pas aidé. Si quelqu'un veut m'aider, il est probable qu'il aura une connaissance plus approfondie de la question que moi, mais peut-être devra-t-il encore effectuer une recherche au préalable. Dans ce cas, les questions leur donnent un point de départ. Et en plus, si un jour quelqu'un atterrit sur cette page mais avec un problème plus similaire à celui auquel je suis lié, il aura un raccourci vers la question appropriée.
T. Verron
@nicael (J'ai oublié de cingler dans le premier message, désolé) Est-ce un problème que les liens ne proviennent pas de mx.sx?
T. Verron
D'accord. Vous pouvez revenir à votre révision, c'est votre message.
nicael

Réponses:

19

Tout d'abord, vous ne devriez pas utiliser accept-process-outputsi vous souhaitez un traitement asynchrone. Emacs acceptera la sortie à chaque fois qu'il attend une entrée utilisateur.

La bonne façon de procéder consiste à utiliser les fonctions de filtrage pour intercepter la sortie. Vous n'avez pas besoin de créer ou de supprimer les filtres selon que vous avez encore des lignes à envoyer. Au lieu de cela, vous déclarerez généralement un seul filtre pour la durée de vie du processus et utiliserez des variables locales de tampon pour suivre l'état et faire différentes choses selon les besoins.

L'interface de bas niveau

Les fonctions de filtrage sont ce que vous recherchez. Les fonctions de filtrage servent à afficher ce que sont les sentinelles jusqu'à la fin.

(defun mymode--output-filter (process string)
  (let ((buffer (process-buffer process)))
    (when (buffer-live-p buffer)
      (with-current-buffer buffer
        (goto-char (point-max))
        (forward-line 0)
        (when (mymode-looking-at-prompt)
          (do-something)
          (goto-char (point-max)))))))

Regardez le manuel ou les nombreux exemples fournis avec Emacs ( greppour process-filterdans les .elfichiers).

Enregistrez votre fonction de filtrage avec

(set-process-filter 'mymode--output-filter)

L'interface comint

Comint définit une fonction de filtre qui fait plusieurs choses:

  • Basculez vers le tampon qui doit contenir la sortie du processus.
  • Exécutez les fonctions de la liste comint-preoutput-filter-functionsen leur passant le nouveau texte comme argument.
  • Effectuez une élimination ponctuelle ponctuelle des doublons, en fonction de comint-prompt-regexp.
  • Insérez la sortie du processus à la fin du tampon
  • Exécutez les fonctions de la liste comint-output-filter-functionsen leur passant le nouveau texte comme argument.

Étant donné que votre mode est basé sur comint, vous devez enregistrer votre filtre dans comint-output-filter-functions. Vous devez définir comint-prompt-regexppour correspondre à votre invite. Je ne pense pas que Comint dispose d'une fonction intégrée pour détecter un morceau de sortie complet (c'est-à-dire entre deux invites), mais cela peut aider. Le marqueur comint-last-input-endest placé à la fin du dernier bloc d'entrée. Vous avez un nouveau bloc de sortie lorsque la fin de la dernière invite est après comint-last-input-end. Comment trouver la fin de la dernière invite dépend de la version d'Emacs:

  • Jusqu'à 24,3, la superposition comint-last-prompt-overlays'étend sur la dernière invite.
  • Depuis la version 24.4, la variable comint-last-promptcontient des marqueurs au début et à la fin de la dernière invite.
(defun mymode--comint-output-filter (string)
  (let ((start (marker-position comint-last-input-end))
        (end (if (boundp 'comint-last-prompt-overlay)
                 (and comint-last-prompt-overlay (overlay-start comint-last-prompt-overlay))
               (and comint-last-prompt (cdr comint-last-prompt))))
  (when (and start end (< start end))
    (let ((new-output-chunk (buffer-substring-no-properties start end)))
      ...)))

Vous souhaiterez peut-être ajouter des protections au cas où le processus émet une sortie dans une séquence autre que {recevoir une entrée, émettre une sortie, afficher une invite}.

Gilles 'SO- arrête d'être méchant'
la source
La variable comint-last-prompt-overlay ne semble pas être définie dans Emacs 25 dans comint.el. Est-ce d'ailleurs?
John Kitchin
@JohnKitchin Cette partie de comint a changé en 24.4, j'avais écrit ma réponse pour 24.3. J'ai ajouté une méthode post-24.4.
Gilles 'SO- arrête d'être méchant'