Estoy tratando de obtener la salida de un subproceso y luego dar comandos a ese proceso en función de la salida anterior. Necesito hacer esto un número variable de veces, cuando el programa necesita más información. (También necesito poder ocultar el símbolo del sistema del subproceso si es posible).

Pensé que esto sería una tarea fácil dado que he visto este problema discutido en publicaciones de 2003 y estamos cerca de 2012 y parece ser una necesidad bastante común y realmente parece que debería ser una parte básica de cualquier lenguaje de programación. Aparentemente, estaba equivocado y de alguna manera, casi 9 años después, ¡todavía no existe una forma estándar de lograr esta tarea de una manera estable, no destructiva e independiente de la plataforma!

Realmente no entiendo mucho sobre la E / S de archivos y el almacenamiento en búfer o subprocesos, por lo que preferiría una solución que sea lo más simple posible. Si hay un módulo que logra esto que es compatible con Python 3.x, estaría muy dispuesto a descargarlo. Me doy cuenta de que hay varias preguntas que plantean básicamente lo mismo, pero todavía tengo que encontrar una respuesta que aborde la tarea simple que estoy tratando de lograr.

Aquí está el código que tengo hasta ahora basado en una variedad de fuentes; sin embargo, no tengo ni idea de qué hacer a continuación. Todos mis intentos terminaron en fracaso y algunos lograron usar el 100% de mi CPU (para no hacer básicamente nada) y no se cerraron.

import subprocess
from subprocess import Popen, PIPE
p = Popen(r'C:\postgis_testing\shellcomm.bat',stdin=PIPE,stdout=PIPE,stderr=subprocess.STDOUT shell=True)
stdout,stdin = p.communicate(b'command string')

En caso de que mi pregunta no esté clara, estoy publicando el texto del archivo por lotes de muestra que demuestra una situación en la que es necesario enviar varios comandos al subproceso (si escribe una cadena de comando incorrecta, el programa se repite).

@echo off
:looper
set INPUT=
set /P INPUT=Type the correct command string:
if "%INPUT%" == "command string" (echo you are correct) else (goto looper)

Si alguien puede ayudarme, se lo agradecería mucho, ¡y estoy seguro de que muchos otros también lo harían!

EDITAR aquí está el código funcional usando el código de eryksun (próxima publicación):

import subprocess
import threading
import time
import sys

try: 
    import queue
except ImportError:
    import Queue as queue

def read_stdout(stdout, q, p):
    it = iter(lambda: stdout.read(1), b'')
    for c in it:
        q.put(c)
        if stdout.closed:
            break

_encoding = getattr(sys.stdout, 'encoding', 'latin-1')
def get_stdout(q, encoding=_encoding):
    out = []
    while 1:
        try:
            out.append(q.get(timeout=0.2))
        except queue.Empty:
            break
    return b''.join(out).rstrip().decode(encoding)

def printout(q):
    outdata = get_stdout(q)
    if outdata:
        print('Output: %s' % outdata)

if __name__ == '__main__':
    #setup
    p = subprocess.Popen(['shellcomm.bat'], stdin=subprocess.PIPE, 
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, 
                     bufsize=0, shell=True) # I put shell=True to hide prompt
    q = queue.Queue()
    encoding = getattr(sys.stdin, 'encoding', 'utf-8')

    #for reading stdout
    t = threading.Thread(target=read_stdout, args=(p.stdout, q, p))
    t.daemon = True
    t.start()

    #command loop
    while p.poll() is None:
        printout(q)
        cmd = input('Input: ')
        cmd = (cmd + '\n').encode(encoding)
        p.stdin.write(cmd)
        time.sleep(0.1) # I added this to give some time to check for closure (otherwise it doesn't work)

    #tear down
    for n in range(4):
        rc = p.poll()
        if rc is not None:
            break
        time.sleep(0.25)
    else:
        p.terminate()
        rc = p.poll()
        if rc is None:
            rc = 1

    printout(q)
    print('Return Code: %d' % rc)

Sin embargo, cuando el script se ejecuta desde un símbolo del sistema, sucede lo siguiente:

C:\Users\username>python C:\postgis_testing\shellcomm7.py
Input: sth
Traceback (most recent call last):
File "C:\postgis_testing\shellcomm7.py", line 51, in <module>
    p.stdin.write(cmd)
IOError: [Errno 22] Invalid argument

Parece que el programa se cierra cuando se ejecuta desde el símbolo del sistema. ¿algunas ideas?

4
THX1138 27 nov. 2011 a las 14:49

1 respuesta

La mejor respuesta

Esta demostración utiliza un hilo dedicado para leer desde stdout. Si busca, estoy seguro de que puede encontrar una implementación más completa escrita en una interfaz orientada a objetos. Al menos puedo decir que esto me está funcionando con su archivo por lotes proporcionado en Python 2.7.2 y 3.2.2.

Shellcomm.bat:

@echo off
echo Command Loop Test
echo.
:looper
set INPUT=
set /P INPUT=Type the correct command string:
if "%INPUT%" == "command string" (echo you are correct) else (goto looper)

Esto es lo que obtengo para la salida basada en la secuencia de comandos "incorrecta", "todavía incorrecta" y "cadena de comandos":

Output:
Command Loop Test

Type the correct command string:
Input: wrong
Output:
Type the correct command string:
Input: still wrong
Output:
Type the correct command string:
Input: command string
Output:
you are correct

Return Code: 0

Para leer la salida canalizada, readline puede funcionar a veces, pero set /P INPUT en el archivo por lotes, naturalmente, no está escribiendo un final de línea. Entonces, en su lugar, usé lambda: stdout.read(1) para leer un byte a la vez (no es tan eficiente, pero funciona). La función de lectura pone los datos en una cola. El hilo principal obtiene la salida de la cola después de escribir un comando. El uso de un tiempo de espera en la llamada get aquí hace que espere una pequeña cantidad de tiempo para asegurarse de que el programa esté esperando una entrada. En su lugar, puede verificar la salida en busca de indicaciones para saber cuándo el programa espera una entrada.

Dicho todo esto, no puede esperar que una configuración como esta funcione universalmente porque el programa de consola con el que está tratando de interactuar puede almacenar en búfer su salida cuando se canaliza. En los sistemas Unix hay algunos comandos de utilidad disponibles que puede insertar en una tubería para modificar el almacenamiento en búfer para que no tenga búfer, búfer de línea o un tamaño determinado, como stdbuf. También hay formas de engañar al programa para que piense que está conectado a un pty (consulte pexpect). Sin embargo, no conozco la forma de solucionar este problema en Windows si no tiene acceso al código fuente del programa para configurar explícitamente el almacenamiento en búfer usando setvbuf.

import subprocess
import threading
import time
import sys

if sys.version_info.major >= 3:
    import queue
else:
    import Queue as queue
    input = raw_input

def read_stdout(stdout, q):
    it = iter(lambda: stdout.read(1), b'')
    for c in it:
        q.put(c)
        if stdout.closed:
            break

_encoding = getattr(sys.stdout, 'encoding', 'latin-1')
def get_stdout(q, encoding=_encoding):
    out = []
    while 1:
        try:
            out.append(q.get(timeout=0.2))
        except queue.Empty:
            break
    return b''.join(out).rstrip().decode(encoding)

def printout(q):
    outdata = get_stdout(q)
    if outdata:
        print('Output:\n%s' % outdata)

if __name__ == '__main__':

    ARGS = ["shellcomm.bat"]   ### Modify this

    #setup
    p = subprocess.Popen(ARGS, bufsize=0, stdin=subprocess.PIPE, 
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    q = queue.Queue()
    encoding = getattr(sys.stdin, 'encoding', 'utf-8')

    #for reading stdout
    t = threading.Thread(target=read_stdout, args=(p.stdout, q))
    t.daemon = True
    t.start()

    #command loop
    while 1:
        printout(q)
        if p.poll() is not None or p.stdin.closed:
            break
        cmd = input('Input: ') 
        cmd = (cmd + '\n').encode(encoding)
        p.stdin.write(cmd)

    #tear down
    for n in range(4):
        rc = p.poll()
        if rc is not None:
            break
        time.sleep(0.25)
    else:
        p.terminate()
        rc = p.poll()
        if rc is None:
            rc = 1

    printout(q)
    print('\nReturn Code: %d' % rc)
3
Eryk Sun 28 nov. 2011 a las 04:20
Muchas gracias por esto, funciona perfectamente en IDLE, pero cuando ejecuto el código modificado (ver mi edición en mi publicación original) me da el siguiente error C: \ Users \ username> python C: \ postgis_testing \ shellcomm7.py Entrada: sth Traceback (última llamada más reciente): Archivo "C: \ postgis_testing \ shellcomm7.py", línea 51, en p.stdin.write (cmd) IOError: [Errno 22] Argumento no válido Parece que el El programa se cierra cuando se ejecuta desde el símbolo del sistema. ¿algunas ideas?
 – 
THX1138
28 nov. 2011 a las 01:55
@ THX1138: Modifiqué el bucle para sondear el proceso y verificar stdin antes de solicitar una entrada. Se rompe si el proceso ha salido. También eliminé el objeto de proceso de los argumentos read_stdout; fue dejado por error de mi primer borrador.
 – 
Eryk Sun
28 nov. 2011 a las 02:43
Cuando ejecuto el nuevo código desde un símbolo del sistema, aparece el error: C: \ Users \ username> python C: \ postgis_testing \ shellcomm8.py Traceback (última llamada más reciente): Archivo "C: \ postgis_testing \ shellcomm8.py" , línea 38, en bufsize = 0) Archivo "C: \ Python32 \ lib \ subprocess.py", línea 741, en init restore_signals, start_new_session) Archivo "C: \ Python32 \ lib \ subprocess.py ", línea 960, en _execute_child startupinfo) WindowsError: [Error 2] El sistema no puede encontrar el archivo especificado
 – 
THX1138
28 nov. 2011 a las 03:19
@ THX1138: Tengo el archivo por lotes shellcomm.bat en el directorio de trabajo actual. Debe llamar a Popen con la ruta completa al archivo por lotes.
 – 
Eryk Sun
28 nov. 2011 a las 03:33
Gracias que funciona, había asumido que puse un nombre de ruta completamente calificado, pero en realidad lo había olvidado.
 – 
THX1138
28 nov. 2011 a las 04:14