LINUX.ORG.RU

Xterm в Tk. Почему не каждый цветной терминальный вывод отображается через Popen+communicate?

 , , , ,


0

1

Имеется процесс xterm, который «вставлен» в Tk-окно:

class Terminal(tk.Frame):

    def __init__(self, parent):
        ...
        # Получение tty с которым будет работать терминал (грубый хак)
        os.system('xterm -into %d -geometry 160x50 -sb -e "tty > /tmp/ttyinfo.txt"' % self.wid)
        fp=open('/tmp/ttyinfo.txt', 'r')
        self.tty=fp.readline().strip();
        fp.close();
        print("tty info: "+self.tty)

        # Открытие терминала с bash. Перед запуском bash выводится рабочий tty
        os.system('xterm -into '+str(self.wid)+' -geometry 160x50 -sb -e "tty ; bash -norc" &')

Выполнение команд в этом xterm-терминале сделано двумя способами:

  • Через os.system().
  • Через Popen+communicate.


Для теста я использую две команды, которые генерируют цветной вывод:

1: ip -color addr
2: ansible-playbook ourPlaybook.yml
И вот какая проблема проявилась:

  • Способ запуска через os.system() всегда работает правильно. Для обеих команд вывод в xterm цветной.
  • Способ запуска через Popen+communicate() правильно работает только для «ip -color addr». Но для Ansible почему-то генерируется монохромный вывод.


А нужно чтобы и через Popen+communicate() правильно генерировался цветной вывод в терминал для любой команды. Потому что только этим способом можно получить stdOut + stdErr + errCode. Ведь в os.system доступен только код возврата.

Для демонстрации я сделал минимальный пример. Функции запуска выглядят так:

    # Выполнение команды в терминале - вариант 1
    def doCommand1(self, text) :
        cmdLine="(%s) <%s >%s 2> %s" % (text, self.tty, self.tty, self.tty)
        result=os.system(cmdLine)
        ...
        return result

    # Выполнение команды в терминале - вариант 2
    def doCommand2(self, text) :
        cmdLine="%s" % (text)
        resultTriada=command.run(cmdLine)
        log.echo("* Результат: "   +resultTriada[0])
        log.echo("* Вывод ошибок: "+resultTriada[1])
        log.echo("* Код возврата: "+str(resultTriada[2]))
        ...
        # Возвращается кортеж из stdOut, stdErr, errCode
        return resultTriada

Во втором случае используется метод с вызовом Popen+communicate():
class Command():

  # Запуск внешней программы
  def run(self, cmd):
    p=Popen(cmd, stdout=PIPE, shell=True)
    stdOutData, stdErrData = p.communicate()
    errCode=p.returncode

    outData=""
    errData=""

    if not stdOutData is None :
      outData=stdOutData.decode()

    if not stdErrData is None :
      errData=stdErrData.decode()

    # Убирается последний перенос строк, чтобы в конце небыло пустой строки
    outData=re.sub("\n$", '', outData)
    errData=re.sub("\n$", '', errData)

    return(outData, errData, errCode)

В примере сделано 4 кнопки (то есть, со всеми возможными вариантами):

- IP адрес (os.system)
- IP адрес (Popen)
- Ansible (os.system)
- Ansible (Popen)

Первые три генерируют цветной вывод, а четвертая - монохромный.

Так вот, надо добиться, чтобы и вариант на 4-й кнопке, обязательно делал цветной вывод.

Ссылка на пример: https://files.fm/u/ex9nfywf2

Скриншот: https://i.ibb.co/FKZQYkt/screenshot-2023-04-25-15-45-51.png
(На нем видна часть цветного вывода 3-й кнопки, и монохромный вывод 4-й кнопки).

★★★★★

Последнее исправление: Xintrea (всего исправлений: 8)

Ответ на: комментарий от eternal_sorrow

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

Кроме того, у меня вся команда представляет собой строку, а не список.

Ну и в-третьих, если заменить параметр на shell=False, то будет ошибка. Непонятно как должны тогда выполняться системные команды:

Traceback (most recent call last):
  File "/home/xi/work/develop/python/xtermSample05/./gui.py", line 16, in <module>
    from libMain import *
  File "./lib/libMain.py", line 15, in <module>
    config=Config()
  File "./lib/libConfig.py", line 14, in __init__
    self.scriptFile=command.run("readlink -e "+sys.argv[0])[0]
  File "./lib/libCommand.py", line 18, in run
    p=Popen(cmd, stdout=PIPE, shell=False)
  File "/usr/lib/python3.9/subprocess.py", line 951, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/lib/python3.9/subprocess.py", line 1823, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'readlink -e ./gui.py'

Xintrea ★★★★★
() автор топика
Последнее исправление: Xintrea (всего исправлений: 2)
Ответ на: комментарий от eternal_sorrow

Shell injection при полном доступе к консоли - это сильно :)

Что по поводу ошибки скажешь? Запускаемую команду надо парсить и разбивать по пробелам? С учетом открывания-закрывания различных видов скобок с учетом возможного экранирования? Как-то это очень нетривиальная задача. А нужно то всего лишь навсего выполнить заданную команду.

Xintrea ★★★★★
() автор топика
Последнее исправление: Xintrea (всего исправлений: 2)
Ответ на: комментарий от Xintrea

Что по поводу ошибки скажешь?

Какой? Той что в ОП? Ничего не скажу. Нагородил какого то огорода, который даже непонятно как отлаживать. Теперь сам разбирайся, в чём у тебя там проблема. Ты знал на что шёл.

eternal_sorrow ★★★★★
()
Последнее исправление: eternal_sorrow (всего исправлений: 1)
Ответ на: комментарий от Xintrea

С учетом открывания-закрывания различных видов скобок с учетом возможного экранирования?

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

eternal_sorrow ★★★★★
()
Последнее исправление: eternal_sorrow (всего исправлений: 1)

во первых : посмотрите какие $TERM получают софтины в обоих случаях. Если TERM выставлен неверно, то софт может работать криво. Агрегат popen+communicate мог выставить TERM по своему усмотрению

во вторых: надо смотреть что там выдаёт в терминал ansible. Он вполне может выдавать esc-запросы и ожидать ответов. (например узнавать геометрию и поддержку цвета). Не получил ответ, включил монохром

MKuznetsov ★★★★★
()

По сути вопроса: наверняка ansible использует isatty.

Не по сути вопроса: почему бы не использовать общепринятые идиомы и стандарты форматирования кода Python? Я понимаю, что все привыкли писать на разных языках, но вот этот стиль Java (или не знаю чего) в Python, об него глаза спотыкаются.

Например, вместо:

os.system('xterm -into '+str(self.wid)+' -geometry 160x50 -sb -e "tty ; bash -norc" &')

можно же написать:

os.system(f'xterm -into {self.wid} -geometry 160x50 -sb -e "tty ; bash -norc" &')

Да тут почти к каждой строке можно придраться. PEP-8 ­— основная скрижаль питонистов — вообще на каждом шагу нарушается.

emorozov
()
Последнее исправление: emorozov (всего исправлений: 1)
Ответ на: комментарий от MKuznetsov

посмотрите какие $TERM получают софтины в обоих случаях

В обеих случаях TERM содержит xterm-256color.

надо смотреть что там выдаёт в терминал ansible. Он вполне может выдавать esc-запросы и ожидать ответов. (например узнавать геометрию и поддержку цвета)

Как бы это сделать? И даже если окажется что это так, то как можно решить проблему в этом случае?

Сейчас нужно определить, почему получается монохром: это терминал его «съедает», или Ansible перестает генерировать цвет. Пока что больше похоже, что Ansible тупит, потому что по кнопке 2 цвет в терминале Popen+communicate есть.

Xintrea ★★★★★
() автор топика
Последнее исправление: Xintrea (всего исправлений: 1)
Ответ на: комментарий от emorozov

По сути вопроса: наверняка ansible использует isatty.

И что нам это дает?

почему бы не использовать общепринятые идиомы и стандарты форматирования кода Python?

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

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Все-таки Asible почему-то цвет отключал.

Многие программы используют isatty, и не выводят цвет, если вывод не связан с терминалом, разумно полагая, что пользователь хочет обработать вывод утилитами типа grep/sed/tr и/или сохранить в файл, и различные управляющие символы ему в этом выводе не нужны.

Совершенно правильное и логичное поведение.

emorozov
()
Ответ на: комментарий от emorozov

Ну да, видимо так и есть.

Я другого понять не могу. Имеется ли возможность «копировать» поток одновременно в несколько приемников?

Что я имею в виду. Некоторые плейбуки Ansible выполняются очень долго, запросто десятки минут. Например если в них сделано копирование больших объемов, или есть циклы, или плейбук инклюдит кучу других плейбуков.

  • В случае использования os.system() информация о ходе работы плейбука сразу попадает в терминал, и видно что в данный момент плейбук делает. Но при этом кроме кода возврата получить информацию о выполненной команде невозможно. То есть, stdOut + stdErr недоступны.
  • В случае использования Popen+communicate, в терминал по ходу работы плейбука ничего не попадает. И поэтому следить за ходом работы плейбука невозможно. Но зато в конце работы можно получить stdOut + stdErr + errCode.


Вопрос: каким макаром можно в варианте Popen+communicate получать выходной поток по ходу работы запущенной команды, чтобы его запихивать в tty который виден в xterm?

Я в doCommand2() заменил

cmdLine="%s" % (text)
resultTriada=command.run(cmdLine)
log.echo("* Результат: "   +resultTriada[0])

на
cmdLine="(%s) <%s >%s 2> %s" % (text, self.tty, self.tty, self.tty)
resultTriada=command.run(cmdLine)
log.echo("* Результат: "   +resultTriada[0])

Это привело к тому, что в терминал данные стали поступать по ходу выполнения команды. Но стандартный вывод в переменной resultTriada[0] стал пустым.

А нужно чтобы и в терминал данные поступали по ходу выполнения команды, и чтобы стандартный вывод в конце работы команды можно было получить в переменную.

Можно ли как-то «разветвить» этот стандартный вывод чтобы и туда и сюда он попадал?

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Я недостаточно подкован в этой области, т.к. никогда не приходилось что-либо подобное делать. Но полагаю, что буферизуется вывод ansible.

Т.к. ansible сам написан на Python, то может быть поможет запускать его с переменной среды PYTHONUNBUFFERED=1?

emorozov
()
Ответ на: комментарий от emorozov

Проблема таки решилась через tee.

Вместо

cmdLine="%s" % (text)

написал
cmdLine="(%s) | tee %s" % (text, self.tty)

И теперь и в xterm данные по ходу выполнения попадают, и в конце выполнения команды результирующий вывод доступен в переменной.

Вроде все хорошо, но появилась другая проблема: Можно ли перенаправить стандартный поток одновременно в несколько приемников? (комментарий)

Xintrea ★★★★★
() автор топика