Aguardar assincronamente a saída de um processo de comint

12

Primeiro de tudo, um aviso. Pesquisei isso muitas vezes e tenho certeza de que já encontrei a resposta de uma maneira ou de outra, mas simplesmente não a entendo.

Meu problema é o seguinte:

  • Eu tenho um processo executando comint
  • Quero enviar uma linha de entrada, capturar saída e ver quando ela termina (quando a última linha da saída corresponde ao regexp para um prompt)
  • somente quando o processo terminar de enviar a saída, desejo enviar outra linha de entrada (por exemplo).

Para um pouco de segundo plano, pense em um modo principal implementando a interação com um programa, que pode retornar uma quantidade arbitrária de saída, em um tempo arbitrariamente longo. Esta não deve ser uma situação incomum, certo? Ok, talvez a parte em que eu preciso esperar entre as entradas seja incomum, mas ela tenha algumas vantagens sobre o envio da entrada como um todo:

  • o buffer de saída está bem formatado: output input input output ...
  • mais importante, ao enviar muito texto para um processo, o texto é cortado em pedaços e os pedaços são colados de volta pelo processo; os pontos de corte são arbitrários e isso às vezes gera uma entrada inválida (meu processo não colará corretamente uma entrada no meio de um identificador, por exemplo)

De qualquer forma, incomum ou não, verifica-se que é complicado. No momento, estou usando algo ao longo das linhas de

(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))))

e eu ligo para isso toda vez depois de enviar uma linha de entrada e antes de enviar a próxima. Bem ... funciona, isso já é alguma coisa.

Mas também faz o emacs travar enquanto aguarda a saída. O motivo é óbvio e achei que, se eu incluísse algum tipo de assíncrono sleep-for(por exemplo, 1s) no loop, isso atrasaria a saída em 1s, mas suprimiria a interrupção. Exceto que parece que esse tipo de assíncrono sleep-for não existe .

Ou faz? De maneira mais geral, existe uma maneira idiomática de conseguir isso com o emacs? Em outras palavras:

Como enviar entrada para um processo, aguardar a saída e enviar mais entradas de forma assíncrona?

Ao pesquisar ao redor (consulte as perguntas relacionadas), vi principalmente menções de sentinelas (mas não acho que isso se aplique no meu caso, já que o processo não termina) e de alguns ganchos comuns (mas então? torne o local do buffer do gancho, transforme em "avaliar as linhas restantes" em uma função, adicione essa função ao gancho e limpe o gancho depois - isso soa muito sujo, não é?).

Sinto muito se não estou me esclarecendo, ou se há realmente uma resposta óbvia disponível em algum lugar, estou realmente confuso com todas as complexidades da interação do processo.

Se necessário, posso fazer disso tudo um exemplo de trabalho, mas tenho medo de que isso faça apenas mais uma "pergunta de processo específica com resposta de processo específica", como todas as que encontrei anteriormente e não me ajudaram.

Algumas perguntas relacionadas ao SO:

T. Verron
fonte
@nicael O que há de errado nos links relacionados?
T. Verron
Mas por que você precisa incluí-los?
Nicael
1
Bem, eu achei que eram perguntas relacionadas, mesmo que as respostas não me ajudassem. Se alguém quiser me ajudar, provavelmente terá um conhecimento mais profundo do assunto do que eu, mas talvez ainda precise realizar uma pesquisa com antecedência. Nesse caso, as perguntas fornecem um ponto de partida. Além disso, se algum dia alguém chegar a esta página, mas com um problema mais parecido com o que eu vinculei, eles terão um atalho para a pergunta apropriada.
506 Verry
@nicael (Esqueceu-se de executar ping no primeiro post, desculpe) É um problema que os links não sejam do mx.sx?
T. Verron
Está bem. Você pode reverter para sua revisão, é sua postagem.
Nicael

Respostas:

19

Primeiro de tudo, você não deve usá- accept-process-outputlo se desejar processamento assíncrono. O Emacs aceita saída sempre que estiver aguardando a entrada do usuário.

O caminho certo a seguir é usar as funções de filtro para interceptar a saída. Você não precisa criar ou excluir o (s) filtro (s), dependendo se você ainda tem linhas para enviar. Em vez disso, você geralmente declara um único filtro para a vida útil do processo e usa variáveis ​​locais de buffer para rastrear o estado e fazer coisas diferentes conforme necessário.

A interface de baixo nível

As funções de filtro são o que você está procurando. As funções de filtro são para mostrar o que são os sentinelas para finalização.

(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)))))))

Olhe para o manual ou em muitos exemplos que vêm com o Emacs ( greppara process-filternos .elarquivos).

Registre sua função de filtro com

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

A interface do comint

Comint define uma função de filtro que faz algumas coisas:

  • Alterne para o buffer que deve conter a saída do processo.
  • Execute as funções na lista comint-preoutput-filter-functions, passando o novo texto como argumento.
  • Realize alguma eliminação de prompt duplicado ad hoc, com base em comint-prompt-regexp.
  • Insira a saída do processo no final do buffer
  • Execute as funções na lista comint-output-filter-functions, passando o novo texto como argumento.

Como seu modo é baseado em comint, você deve registrar seu filtro comint-output-filter-functions. Você deve definir comint-prompt-regexppara corresponder ao seu prompt. Não acho que o Comint tenha um recurso interno para detectar um pedaço de saída completo (ou seja, entre dois prompts), mas pode ajudar. O marcador comint-last-input-endé definido no final do último pedaço de entrada. Você tem um novo bloco de saída quando o final do último prompt é posterior comint-last-input-end. Como encontrar o final do último prompt depende da versão do Emacs:

  • Até 24,3, a sobreposição comint-last-prompt-overlayabrange o último prompt.
  • Desde 24.4, a variável comint-last-promptcontém marcadores para o início e o fim do último prompt.
(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)))
      ...)))

Você pode adicionar proteções caso o processo emita uma saída em uma sequência diferente de {receber entrada, emitir saída, exibir prompt}.

Gilles 'SO- parar de ser mau'
fonte
A variável comint-last-prompt-overlay não parece ser definida no Emacs 25 em comint.el. É de outro lugar?
John Kitchin
@ JohnKitchin Essa parte do comint mudou em 24.4, eu escrevi minha resposta para 24.3. Eu adicionei um método pós-24.4.
Gilles 'SO- stop be evil'