Подпроцесс Python с вводом в реальном времени и несколькими консолями - PullRequest
0 голосов
/ 30 ноября 2018

Основная проблема

В двух словах: я хочу две консоли для моей программы.Один для активного ввода пользователя.А другой для чистого вывода журнала. (Рабочий код, включающий принятый ответ, приведен в тексте вопроса ниже, в разделе «Редактирование-3». И в разделе «Редактирование-1» и раздел «Редактирование-2 "работают обходные пути.)

Для этого у меня есть сценарий Python для командной строки, который должен открывать дополнительную консоль только для вывода журнала.Для этого я намерен перенаправить вывод журнала, который будет напечатан на консоли основного сценария, на стандартный ввод второй консоли, который я запускаю как подпроцесс.(Я использую подпроцесс, потому что я не нашел другого способа открыть вторую консоль.)

Проблема в том, что мне кажется, что я могу отправить на стандартный ввод второй консоли - однакона второй консоли ничего не печатается.

Ниже приведен код, который я использовал для экспериментов (с Python 3.4 на PyDev под Windows 10).Функция writing(input, pipe, process) содержит часть, в которой сгенерированная строка копируется в как pipe переданный stdin открытой консоли подпроцесса.Запись функции (...) выполняется через класс writetest(Thread).(Я оставил некоторый код, который я закомментировал.)

import os
import sys
import io
import time
import threading
from cmd import Cmd
from queue import Queue
from subprocess import Popen, PIPE, CREATE_NEW_CONSOLE


REPETITIONS = 3


# Position of "The class" (Edit-2)


# Position of "The class" (Edit-1)


class generatetest(threading.Thread):

    def __init__(self, queue):
        self.output = queue
        threading.Thread.__init__(self)

    def run(self):
        print('run generatetest')
        generating(REPETITIONS, self.output)
        print('generatetest done')

    def getout(self):
        return self.output


class writetest(threading.Thread):

    def __init__(self, input=None, pipe=None, process=None):
        if (input == None):        # just in case
            self.input = Queue()
        else:
            self.input = input

        if (pipe == None):        # just in case
            self.pipe = PIPE
        else:
            self.pipe = pipe

        if (process == None):        # just in case
            self.process = subprocess.Popen('C:\Windows\System32\cmd.exe', universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)
        else:
            self.process = proc

        threading.Thread.__init__(self)

    def run(self):
        print('run writetest')
        writing(self.input, self.pipe, self.process)
        print('writetest done')


# Position of "The function" (Edit-2)


# Position of "The function" (Edit-1)


def generating(maxint, outline):
    print('def generating')
    for i in range(maxint):
        time.sleep(1)
        outline.put_nowait(i)


def writing(input, pipe, process):
    print('def writing')
    while(True):
        try:
            print('try')
            string = str(input.get(True, REPETITIONS)) + "\n"
            pipe = io.StringIO(string)
            pipe.flush()
            time.sleep(1)
            # print(pipe.readline())
        except:
            print('except')
            break
        finally:
            print('finally')
            pass


data_queue = Queue()
data_pipe = sys.stdin
# printer = sys.stdout
# data_pipe = os.pipe()[1]


# The code of 'C:\\Users\\Public\\Documents\\test\\test-cmd.py'
# can be found in the question's text further below under "More code"


exe = 'C:\Python34\python.exe'
# exe = 'C:\Windows\System32\cmd.exe'
arg = 'C:\\Users\\Public\\Documents\\test\\test-cmd.py'
arguments = [exe, arg]
# proc = Popen(arguments, universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)
proc = Popen(arguments, stdin=data_pipe, stdout=PIPE, stderr=PIPE,
             universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)


# Position of "The call" (Edit-2 & Edit-1) - file init (proxyfile)


# Position of "The call" (Edit-2) - thread = sockettest()
# Position of "The call" (Edit-1) - thread0 = logtest()
thread1 = generatetest(data_queue)
thread2 = writetest(data_queue, data_pipe, proc)
# time.sleep(5)


# Position of "The call" (Edit-2) - thread.start()
# Position of "The call" (Edit-1) - thread0.start()
thread1.start()
thread2.start()


# Position of "The call" (Edit-2) - thread.join()
# Position of "The call" (Edit-1) - thread.join()
thread1.join(REPETITIONS * REPETITIONS)
thread2.join(REPETITIONS * REPETITIONS)

# data_queue.join()
# receiver = proc.communicate(stdin, 5)
# print('OUT:' + receiver[0])
# print('ERR:' + receiver[1])

print("1st part finished")

Немного другой подход

Следующий дополнительный фрагмент кода работает для извлечения stdout из подпроцесса.Однако ранее отправленный стандартный ввод все еще не распечатывается на второй консоли.Кроме того, вторая консоль закрывается немедленно.

proc2 = Popen(['C:\Python34\python.exe', '-i'],
              stdin=PIPE,
              stdout=PIPE,
              stderr=PIPE,
              creationflags=CREATE_NEW_CONSOLE)
proc2.stdin.write(b'2+2\n')
proc2.stdin.flush()
print(proc2.stdout.readline())
proc2.stdin.write(b'len("foobar")\n')
proc2.stdin.flush()
print(proc2.stdout.readline())
time.sleep(1)
proc2.stdin.close()
proc2.terminate()
proc2.wait(timeout=0.2)

print("Exiting Main Thread")

Дополнительная информация

Как только я использую один из параметров stdin=data_pipe, stdout=PIPE, stderr=PIPE для запуска подпроцесса, получается вторая консольне активен и не принимает ввод с клавиатуры (что нежелательно, хотя здесь может быть полезна информация).

Метод подпроцесса communicate() не может использоваться для этого, так как он ожидаетпроцесс до конца.


Больше кода

Наконец код для файла, который предназначен для второй консоли.

C: \ Users \ Public \ Documents \test \ test-cmd.py

from cmd import Cmd
from time import sleep
from datetime import datetime

INTRO = 'command line'
PROMPT = '> '


class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, intro=INTRO, prompt=PROMPT):
        Cmd.__init__(self)
        self.intro = intro
        self.prompt = prompt
        self.doc_header = intro
        self.running = False

    def do_dummy(self, args):
        """Runs a dummy method."""
        print("Do the dummy.")
        self.running = True
        while(self.running == True):
            print(datetime.now())
            sleep(5)

    def do_stop(self, args):
        """Stops the dummy method."""
        print("Stop the dummy, if you can.")
        self.running = False

    def do_exit(self, args):
        """Exits this console."""
        print("Do console exit.")
        exit()

if __name__ == '__main__':
    cl = CommandLine()
    cl.prompt = PROMPT
    cl.cmdloop(INTRO)

Мысли

Пока что я даже не уверен, предлагает ли интерфейс командной строки Windows возможность принимать другие данные, кроме той, чтос клавиатуры (вместо желаемой трубы stdin или аналогичной).Тем не менее, учитывая наличие пассивного режима, я ожидаю.

Почему это не работает?


Edit-1: Обход через файл (подтверждение концепции)

Использование файла в качестве обходного пути для отображения порядка его нового содержимого, как предлагается в ответе Работа с несколькими консолями в python , в целом работает.Однако, поскольку размер файла журнала увеличится до многих ГБ, в данном случае это не практичное решение.По крайней мере, потребуется разделение файла и правильная обработка.

Класс:

class logtest(threading.Thread):

    def __init__(self, file):
        self.file = file
        threading.Thread.__init__(self)

    def run(self):
        print('run logtest')
        logging(self.file)
        print('logtest done')

Функция:

def logging(file):
    pexe = 'C:\Python34\python.exe '
    script = 'C:\\Users\\Public\\Documents\\test\\test-004.py'
    filek = '--file'
    filev = file

    file = open(file, 'a')
    file.close()
    time.sleep(1)

    print('LOG START (outer): ' + script + ' ' + filek + ' ' + filev)
    proc = Popen([pexe, script, filek, filev], universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)
    print('LOG FINISH (outer): ' + script + ' ' + filek + ' ' + filev)

    time.sleep(2)

Вызов:

# The file tempdata is filled with several strings of "0\n1\n2\n"
# Looking like this:
# 0
# 1
# 2
# 0
# 1
# 2

proxyfile = 'C:\\Users\\Public\\Documents\\test\\tempdata'
f = open(proxyfile, 'a')
f.close()
time.sleep(1)

thread0 = logtest(proxyfile)
thread0.start()
thread0.join(REPETITIONS * REPETITIONS)

Сценарий хвоста ("test-004.py"):

Поскольку Windows не предлагает команду хвоста, я использовал вместо этого следующий сценарий (основываясь на ответе для Как реализовать питонический эквивалент tail -F? ), который сработал для этого.Дополнительный, но отчасти ненужный class CommandLine(Cmd) изначально был попыткой сохранить вторую консоль открытой (поскольку отсутствовал аргумент файла сценария).Тем не менее, он также зарекомендовал себя как полезный инструмент для быстрой печати на консоли содержимого нового файла журнала.В противном случае выходные данные не были бы детерминированными / предсказуемыми.

import time
import sys
import os
import threading
from cmd import Cmd
from argparse import ArgumentParser


def main(args):
    parser = ArgumentParser(description="Parse arguments.")
    parser.add_argument("-f", "--file", type=str, default='', required=False)
    arguments = parser.parse_args(args)

    if not arguments.file:
        print('LOG PRE-START (inner): file argument not found. Creating new default entry.')
        arguments.file = 'C:\\Users\\Public\\Documents\\test\\tempdata'

    print('LOG START (inner): ' + os.path.abspath(os.path.dirname(__file__)) + ' ' + arguments.file)

    f = open(arguments.file, 'a')
    f.close()
    time.sleep(1)

    words = ['word']
    console = CommandLine(arguments.file, words)
    console.prompt = ''

    thread = threading.Thread(target=console.cmdloop, args=('', ))
    thread.start()
    print("\n")

    for hit_word, hit_sentence in console.watch():
        print("Found %r in line: %r" % (hit_word, hit_sentence))

    print('LOG FINISH (inner): ' + os.path.abspath(os.path.dirname(__file__)) + ' ' + arguments.file)


class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, fn, words):
        Cmd.__init__(self)
        self.fn = fn
        self.words = words

    def watch(self):
        fp = open(self.fn, 'r')
        while True:
            time.sleep(0.05)
            new = fp.readline()
            print(new)
            # Once all lines are read this just returns ''
            # until the file changes and a new line appears

            if new:
                for word in self.words:
                    if word in new:
                        yield (word, new)

            else:
                time.sleep(0.5)


if __name__ == '__main__':
    print('LOG START (inner - as main).')
    main(sys.argv[1:])

Edit-1: Больше мыслей

Три обходных пути, которые я еще не пробовал и которые могли бы работать, - это сокеты (также предлагается в этом ответе Работа с несколькими консолями в python ), получение объекта процесса через идентификатор процесса для большего контроля, ииспользование библиотеки ctypes для прямого доступа к API консоли Windows, что позволяет устанавливать экранный буфер, поскольку консоль может иметь несколько буферов, но только один активный буфер (указано в примечаниях к документации для функции CreateConsoleScreenBuffer ).

Однако проще всего использовать сокеты.И по крайней мере размер журнала не имеет значения таким образом.Тем не менее, проблемы с подключением могут быть проблемой здесь.


Edit-2: Обход через сокеты (подтверждение концепции)

Использование сокетов в качестве обходного пути для отображения новых журналов, как этоТакже было предложено в ответе Работа нескольких консолей в Python , тоже работает в общем.Тем не менее, это кажется слишком большим усилием для чего-то, что должно быть просто отправлено процессу принимающей консоли.

Класс:

class sockettest(threading.Thread):

    def __init__(self, host, port, file):
        self.host = host
        self.port = port
        self.file = file
        threading.Thread.__init__(self)

    def run(self):
        print('run sockettest')
        socketing(self.host, self.port, self.file)
        print('sockettest done')

Функция:

def socketing(host, port, file):
    pexe = 'C:\Python34\python.exe '
    script = 'C:\\Users\\Public\\Documents\\test\test-005.py'
    hostk = '--address'
    hostv = str(host)
    portk = '--port'
    portv = str(port)
    filek = '--file'
    filev = file

    file = open(file, 'a')
    file.close()
    time.sleep(1)

    print('HOST START (outer): ' + pexe + script + ' ' + hostk + ' ' + hostv + ' ' + portk + ' ' + portv + ' ' + filek + ' ' + filev)
    proc = Popen([pexe, script, hostk, hostv, portk, portv, filek, filev], universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)

    print('HOST FINISH (outer): ' + pexe + script + ' ' + hostk + ' ' + hostv + ' ' + portk + ' ' + portv + ' ' + filek + ' ' + filev)

    time.sleep(2)

Вызов:

# The file tempdata is filled with several strings of "0\n1\n2\n"
# Looking like this:
# 0
# 1
# 2
# 0
# 1
# 2

proxyfile = 'C:\\Users\\Public\\Documents\\test\\tempdata'
f = open(proxyfile, 'a')
f.close()
time.sleep(1)

thread = sockettest('127.0.0.1', 8888, proxyfile)
thread.start()
thread.join(REPETITIONS * REPETITIONS)

Сценарий сокета ("test-005.py"):

Следующий скрипт основан на Python: сервер программирования сокетов-клиент приложения с использованием потоков .Здесь я просто сохранил class CommandLine(Cmd) как генератор записей журнала.На этом этапе не должно быть проблемой, чтобы поместить клиента в основной скрипт, который вызывает вторую консоль, а затем заполнить очередь реальными записями вместо (новых) строк файла.(Сервер является принтером.)

import socket
import sys
import threading
import time
from cmd import Cmd
from argparse import ArgumentParser
from queue import Queue

BUFFER_SIZE = 5120

class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, fn, words, queue):
        Cmd.__init__(self)
        self.fn = fn
        self.words = words
        self.queue = queue

    def watch(self):
        fp = open(self.fn, 'r')
        while True:
            time.sleep(0.05)
            new = fp.readline()

            # Once all lines are read this just returns ''
            # until the file changes and a new line appears
            self.queue.put_nowait(new)


def main(args):
    parser = ArgumentParser(description="Parse arguments.")
    parser.add_argument("-a", "--address", type=str, default='127.0.0.1', required=False)
    parser.add_argument("-p", "--port", type=str, default='8888', required=False)
    parser.add_argument("-f", "--file", type=str, default='', required=False)
    arguments = parser.parse_args(args)

    if not arguments.address:
        print('HOST PRE-START (inner): host argument not found. Creating new default entry.')
        arguments.host = '127.0.0.1'
    if not arguments.port:
        print('HOST PRE-START (inner): port argument not found. Creating new default entry.')
        arguments.port = '8888'
    if not arguments.file:
        print('HOST PRE-START (inner): file argument not found. Creating new default entry.')
        arguments.file = 'C:\\Users\\Public\\Documents\\test\\tempdata'

    file_queue = Queue()

    print('HOST START (inner): ' + ' ' + arguments.address + ':' + arguments.port + ' --file ' + arguments.file)

    # Start server
    thread = threading.Thread(target=start_server, args=(arguments.address, arguments.port, ))
    thread.start()
    time.sleep(1)

    # Start client
    thread = threading.Thread(target=start_client, args=(arguments.address, arguments.port, file_queue, ))
    thread.start()

    # Start file reader
    f = open(arguments.file, 'a')
    f.close()
    time.sleep(1)

    words = ['word']
    console = CommandLine(arguments.file, words, file_queue)
    console.prompt = ''

    thread = threading.Thread(target=console.cmdloop, args=('', ))
    thread.start()
    print("\n")

    for hit_word, hit_sentence in console.watch():
        print("Found %r in line: %r" % (hit_word, hit_sentence))

    print('HOST FINISH (inner): ' + ' ' + arguments.address + ':' + arguments.port)


def start_client(host, port, queue):
    host = host
    port = int(port)         # arbitrary non-privileged port
    queue = queue

    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        soc.connect((host, port))
    except:
        print("Client connection error" + str(sys.exc_info()))
        sys.exit()

    print("Enter 'quit' to exit")
    message = ""

    while message != 'quit':
        time.sleep(0.05)
        if(message != ""):
            soc.sendall(message.encode("utf8"))
            if soc.recv(BUFFER_SIZE).decode("utf8") == "-":
                pass        # null operation

        string = ""
        if (not queue.empty()):
            string = str(queue.get_nowait()) + "\n"

        if(string == None or string == ""):
            message = ""
        else:
            message = string

    soc.send(b'--quit--')


def start_server(host, port):
    host = host
    port = int(port)         # arbitrary non-privileged port

    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # SO_REUSEADDR flag tells the kernel to reuse a local socket in TIME_WAIT state, without waiting for its natural timeout to expire
    soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    print("Socket created")

    try:
        soc.bind((host, port))
    except:
        print("Bind failed. Error : " + str(sys.exc_info()))
        sys.exit()

    soc.listen(5)       # queue up to 5 requests
    print("Socket now listening")

    # infinite loop- do not reset for every requests
    while True:
        connection, address = soc.accept()
        ip, port = str(address[0]), str(address[1])
        print("Connected with " + ip + ":" + port)

        try:
            threading.Thread(target=client_thread, args=(connection, ip, port)).start()
        except:
            print("Thread did not start.")
            traceback.print_exc()

    soc.close()


def client_thread(connection, ip, port, max_buffer_size=BUFFER_SIZE):
    is_active = True

    while is_active:
        client_input = receive_input(connection, max_buffer_size)

        if "--QUIT--" in client_input:
            print("Client is requesting to quit")
            connection.close()
            print("Connection " + ip + ":" + port + " closed")
            is_active = False
        elif not client_input == "":
            print("{}".format(client_input))
            connection.sendall("-".encode("utf8"))
        else:
            connection.sendall("-".encode("utf8"))


def receive_input(connection, max_buffer_size):
    client_input = connection.recv(max_buffer_size)
    client_input_size = sys.getsizeof(client_input)

    if client_input_size > max_buffer_size:
        print("The input size is greater than expected {}".format(client_input_size))

    decoded_input = client_input.decode("utf8").rstrip()  # decode and strip end of line
    result = process_input(decoded_input)

    return result


def process_input(input_str):
    return str(input_str).upper()


if __name__ == '__main__':
    print('HOST START (inner - as main).')
    main(sys.argv[1:])

Edit-2: Более того, мысли

Наличие прямого контроля над каналом / буфером ввода подпрограммы было бы предпочтительным решением дляЭта проблема.Ибо это награда в 500 репутаций.

К сожалению, у меня мало времени.Поэтому я мог бы сейчас использовать один из этих обходных путей и позже заменить его на правильное решение.Или, может быть, мне нужно использовать ядерную опцию, только одну консоль, где текущий вывод журнала приостанавливается во время любого ввода с клавиатуры пользователя, а затем печатается.Конечно, это может привести к проблемам с буфером, когда пользователь решит набрать что-то наполовину.


Edit-3: код, включающий принятый ответ (один файл)

СВ ответ от Джеймса Кента я получаю желаемое поведение, когда запускаю скрипт с кодом через командную строку Windows (cmd) или PowerShell.Однако, когда я запускаю этот же скрипт через Eclipse / PyDev с «Python run», выходные данные всегда печатаются на главной консоли Eclipse / PyDev, в то время как вторая консоль подпроцесса остается пустой и остается неактивной.Хотя, я полагаю, это еще одна особенность системы / среды и другая проблема.

from sys import argv, stdin, stdout
from threading import Thread
from cmd import Cmd
from time import sleep
from datetime import datetime
from subprocess import Popen, PIPE, CREATE_NEW_CONSOLE

INTRO = 'command line'
PROMPT = '> '


class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, subprocess, intro=INTRO, prompt=PROMPT):
        Cmd.__init__(self)
        self.subprocess = subprocess
        self.intro = intro
        self.prompt = prompt
        self.doc_header = intro
        self.running = False

    def do_date(self, args):
        """Prints the current date and time."""
        print(datetime.now())
        sleep(1)

    def do_exit(self, args):
        """Exits this command line application."""
        print("Exit by user command.")
        if self.subprocess is not None:
            try:
                self.subprocess.terminate()
            except:
                self.subprocess.kill()
        exit()


class Console():

    def __init__(self):
        if '-r' not in argv:
            self.p = Popen(
                ['python.exe', __file__, '-r'],
                stdin=PIPE,
                creationflags=CREATE_NEW_CONSOLE
            )
        else:
            while True:
                data = stdin.read(1)
                if not data:
                    #                     break
                    sleep(1)
                    continue
                stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

    def getSubprocess(self):
        if self.p:
            return self.p
        else:
            return None


class Feeder (Thread):

    def __init__(self, console):
        self.console = console
        Thread.__init__(self)

    def run(self):
        feeding(self.console)


def feeding(console):
    for i in range(0, 100):
        console.write('test %i\n' % i)
        sleep(1)


if __name__ == '__main__':
    p = Console()
    if '-r' not in argv:
        thread = Feeder(p)
        thread.setDaemon(True)
        thread.start()

        cl = CommandLine(subprocess=p.getSubprocess())
        cl.use_rawinput = False
        cl.prompt = PROMPT
        cl.cmdloop('\nCommand line is waiting for user input (e.g. help).')

Edit-3: Достойное упоминание

В приведенном выше тексте вопросов я упоминал об использованииБиблиотека ctypes для прямого доступа к консольному API Windows в качестве другого рабочего пространства (в разделе «Правка-1: Больше мыслей»).Или используя только одну консоль таким образом, чтобы подсказка ввода всегда оставалась внизу как ядерный вариант всей этой проблемы.(в разделе «Правка-2: Дополнительные мысли»)

Для использования библиотеки ctypes я бы ориентировался на следующий ответ на Изменить шрифт консоли в Windows .А для использования только одной консоли я бы попробовал следующий ответ на Оставить строку ввода консоли ниже вывода .Я думаю, что оба эти ответа могут предложить потенциальную заслугу в отношении этой проблемы, и, возможно, они полезны для других, как это происходит через этот пост.Кроме того, я, если найду время, попробую, если они как-нибудь сработают.

1 Ответ

0 голосов
/ 04 декабря 2018

Проблема, с которой вы сталкиваетесь, заключается в архитектуре консольной подсистемы в Windows. Обычно видимое окно консоли не размещается в cmd.exe, а вместо этого в conhost.exe дочерний процесс окна conhost может толькоподключиться к одному экземпляру conhost, что означает, что вы ограничены одним окном на процесс.

Это затем приводит к созданию дополнительного процесса для каждого окна консоли, которое вы хотите иметь, а затем для просмотра отображения чего-либов этом окне вам нужно посмотреть, как обычно обрабатываются stdin и stdout, в том смысле, что они записываются и читаются экземпляром conhost, за исключением случаев, когда вы превращаете stdin в канал (так что вы можете записывать в процесс), он больше не приходитиз conhost, но вместо этого из вашего родительского процесса и как таковой conhost не имеет его видимости.Это означает, что все, что записано в stdin, читается только дочерним процессом, поэтому не отображается conhost.

Насколько я знаю, такого способа передачи канала не существует.

В качестве побочного эффекта, если вы сделаете stdin каналом, тогда весь ввод с клавиатуры, отправленный в новое окно консоли, не будет выполнен, так как stdin не подключен к этому окну.

Для функции только для вывода это означает, что вы можете порождатьновый процесс, который связывается с родителем через канал к stdin и выводит все в stdout.

Вот попытка:

#!python3

import sys, subprocess, time

class Console():
    def __init__(self):
        if '-r' not in sys.argv:
            self.p = subprocess.Popen(
                ['python.exe', __file__, '-r'],
                stdin=subprocess.PIPE,
                creationflags=subprocess.CREATE_NEW_CONSOLE
                )
        else:
            while True:
                data = sys.stdin.read(1)
                if not data:
                    break
                sys.stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

if (__name__ == '__main__'):
    p = Console()
    if '-r' not in sys.argv:
        for i in range(0, 100):
            p.write('test %i\n' % i)
            time.sleep(1)

Итак, хороший простой канал между двумя процессами и повторение ввода обратнона выходе, если это подпроцесс, я использовал -r, чтобы указать, является ли экземпляр процессом, но есть и другие способы, в зависимости от того, как вы его реализуете.

Несколько вещей, на которые следует обратить внимание:

  • необходима очистка после записи в stdin, поскольку python обычно использует буферизацию.
  • способ написания этого подхода направлен на то, чтобы быть в своем собственном модуле.Следовательно, использование __file__
  • из-за использования __file__ может потребовать модификации этого подхода, если он заморожен с использованием cx_Freeze или аналогичного.

РЕДАКТИРОВАТЬ 1

для версии, которая может быть заморожена с помощью cx_Freeze:

Console.py

import sys, subprocess

class Console():
    def __init__(self, ischild=True):
        if not ischild:
            if hasattr(sys, 'frozen'):
                args = ['Console.exe']
            else:
                args = [sys.executable, __file__]
            self.p = subprocess.Popen(
                args,
                stdin=subprocess.PIPE,
                creationflags=subprocess.CREATE_NEW_CONSOLE
                )
        else:
            while True:
                data = sys.stdin.read(1)
                if not data:
                    break
                sys.stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

if (__name__ == '__main__'):
    p = Console()

test.py

from Console import Console
import sys, time

if (__name__ == '__main__'):
    p = Console(False)
    for i in range(0, 100):
        p.write('test %i\n' % i)
        time.sleep(1)

setup.py

from cx_Freeze import setup, Executable

setup(
    name = 'Console-test',
    executables = [
        Executable(
            'Console.py',
            base=None,
            ),
        Executable(
            'test.py',
            base=None,
            )
        ]
)

РЕДАКТИРОВАТЬ 2

Новая версия, которая должна работать под такими инструментами разработчика, как IDLE

Console.py

#!python3

import ctypes, sys, subprocess

Kernel32 = ctypes.windll.Kernel32

class Console():
    def __init__(self, ischild=True):
        if ischild:
            # try allocate new console
            result = Kernel32.AllocConsole()
            if result > 0:
                # if we succeed open handle to the console output
                sys.stdout = open('CONOUT$', mode='w')
        else:
            # if frozen we assume its names Console.exe
            # note that when frozen 'Win32GUI' must be used as a base
            if hasattr(sys, 'frozen'):
                args = ['Console.exe']
            else:
                # otherwise we use the console free version of python
                args = ['pythonw.exe', __file__]
            self.p = subprocess.Popen(
                args,
                stdin=subprocess.PIPE
                )
            return
        while True:
            data = sys.stdin.read(1)
            if not data:
                break
            sys.stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

if (__name__ == '__main__'):
    p = Console()

test.py

from Console import Console
import sys, time

if (__name__ == '__main__'):
    p = Console(False)
    for i in range(0, 100):
        p.write('test %i\n' % i)
        time.sleep(1)

setup.py

from cx_Freeze import setup, Executable

setup(
    name = 'Console-test',
    executables = [
        Executable(
            'Console.py',
            base='Win32GUI',
            ),
        Executable(
            'test.py',
            base=None,
            )
        ]
)

Это можно сделать более надежным, то есть всегда проверять существующую консоль и отсоединять ее, если она найдена, перед созданием новой консоли, и, возможно, улучшить обработку ошибок.

...