Подпроцессы Python не выводятся должным образом?

Я не думаю, что вообще правильно понимаю подпроцесс python, но вот простой пример, иллюстрирующий момент, который меня смущает:

#!/usr/bin/env python
import subprocess
lookup_server = subprocess.Popen("nc -l 5050", shell=True)
lookup_client = subprocess.Popen("nc localhost 5050", shell=True, stdin=subprocess.PIPE) 
print lookup_client.poll()
lookup_client.stdin.write("magic\n")
print lookup_client.poll()                                                                        
lookup_client.send_signal(subprocess.signal.SIGINT)
print lookup_client.poll()
lookup_server.wait()
print "Lookup server terminated properly"

Вывод возвращается как

None
None
None

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

В конце концов, я столкнулся с проблемой в гораздо более крупной программе, которая делает что-то подобное, используя netcat и другую программу, работающую локально, вместо двух версий nc. В любом случае, я не мог писать или читать с них должным образом. Однако, когда я запускаю их в консоли Python, все работает так, как я и ожидал. Все это меня очень расстроило. Дайте мне знать, если у вас есть идеи!

РЕДАКТИРОВАТЬ: я запускаю это на Ubuntu Linux 12.04, когда я man nc, я получаю руководство по основным командам BSD, поэтому я предполагаю, что это BSD netcat.


person jrbalsano    schedule 20.11.2012    source источник
comment
Кроме того, вы все еще используете shell=True при использовании массива аргументов вместо строки? А вы на какой платформе? В Unix с shell=True, если args является последовательностью, первый элемент определяет командную строку, а любые дополнительные элементы будут рассматриваться как дополнительные аргументы для самой оболочки.   -  person abarnert    schedule 21.11.2012
comment
Кроме того, в этом простом случае вы можете заменить все элементы lookup_client одной строкой: lookup_client.communicate("magic\n"). Это отправит строку в ее stdin, отправит EOF, а затем дождется ее выхода, что именно то, что вы хотите. Это может быть неприменимо для вашего реального варианта использования.   -  person abarnert    schedule 21.11.2012
comment
Кроме того, вы рассматривали возможность использования pexpect или чего-то в этом роде? Для обработки таких взаимодействий может быть проще использовать pexpect.   -  person zzzirk    schedule 21.11.2012
comment
Я не использовал вызов сообщения, потому что в реальной программе мне нужно отправить несколько строк через nc. Коммуникационный вызов закрывает стандартный ввод после одного использования, и это было бы невероятно проблематично с моей точки зрения.   -  person jrbalsano    schedule 21.11.2012
comment
Хорошо, затем несколько раз вызовите write и close, когда закончите. Теоретически это не совсем безопасно, потому что буфер stdin клиента может заблокироваться, так что вам нужно делать все те причудливые вещи, которые communicate делает под капотом; на практике я не думаю, что это будет проблемой с nc.   -  person abarnert    schedule 21.11.2012


Ответы (2)


Проблема здесь в том, что вы отправляете SIGINT процессу. Если вы просто close stdin, nc закроет свой сокет и выйдет, что вы и хотите.

Похоже, вы на самом деле используете nc для клиента (хотя и не для сервера) в своей реальной программе, что означает, что у вас есть два простых исправления:

Вместо lookup_client.send_signal(subprocess.signal.SIGINT) просто сделайте lookup_client.stdin.close(). nc увидит это как EOF на своем входе и нормально завершит работу, после чего ваш сервер также завершит работу.

#!/usr/bin/env python
import subprocess
lookup_server = subprocess.Popen("nc -l 5050", shell=True)
lookup_client = subprocess.Popen("nc localhost 5050", shell=True, stdin=subprocess.PIPE) 
print lookup_client.poll()
lookup_client.stdin.write("magic\n")
lookup_client.stdin.close()
print lookup_client.poll()
lookup_server.wait()
print "Lookup server terminated properly"

Когда я запускаю это, наиболее распространенный вывод:

None
None
magic
Lookup server terminated properly

Иногда вторая None вместо 0 и/или она идет после magic, а не до, но в противном случае это всегда все четыре строки. (Я работаю на OS X.)

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

#!/usr/bin/env python
import subprocess
lookup_server = subprocess.Popen("nc -l 5050", shell=True)
lookup_client = subprocess.Popen("nc localhost 5050", shell=True, stdin=subprocess.PIPE) 
print lookup_client.communicate("magic\n")
lookup_server.wait()
print "Lookup server terminated properly"

Тем временем:

Кроме того, если я изменю первый аргумент Popen на массив всех этих аргументов, ни один из вызовов nc не будет выполняться должным образом, и сценарий будет выполняться без ожидания. Почему это происходит?

Как сказано в в документах:

В Unix с shell=True… Если args является последовательностью, первый элемент определяет командную строку, а любые дополнительные элементы будут рассматриваться как дополнительные аргументы для самой оболочки.

Итак, subprocess.Popen(["nc", "-l", "5050"], shell=True) делает /bin/sh -c 'nc' -l 5050, а sh не знает, что делать с этими аргументами.

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

Еще кое-что:

lookup_client.send_signal(subprocess.signal.SIGINT)
print lookup_client.poll()

Это может вывести либо -2, либо None, в зависимости от того, закончил ли клиент отвечать на SIGINT и был ли он убит до того, как вы его poll. Если вы действительно хотите получить это -2, вам нужно вызвать wait, а не poll (или сделать что-то еще, например выполнить цикл до тех пор, пока poll не вернет значение, отличное от None).

Наконец, почему ваш исходный код не работал? Ну, отправка SIGINT асинхронна; нет никакой гарантии относительно того, когда это может вступить в силу. В качестве одного из примеров того, что может пойти не так, это может произойти еще до того, как клиент откроет сокет, и в этом случае сервер все еще сидит в ожидании клиента, который никогда не появляется.

Вы можете добавить time.sleep(5) перед вызовом signal, чтобы проверить это, но очевидно, что это не настоящее исправление или даже приемлемый хак; это полезно только для тестирования проблемы. Что вам нужно сделать, так это не убивать клиента, пока он не сделает все, что вы от него хотите. Для сложных случаев вам нужно будет создать какой-то механизм для этого (например, чтение его stdout), в то время как для простых случаев communicate уже все, что вам нужно (и в первую очередь нет причин убивать ребенка).

person abarnert    schedule 20.11.2012
comment
Это очень помогает, я собираюсь попробовать это реализовать, но у меня есть один вопрос: почему в этом случае вывод netcat не идет в оболочку? Я никогда не пытался перенаправить, поэтому я подумал, что stdout/err все равно должен идти по тому же маршруту, что и стандартный вывод python? - person jrbalsano; 21.11.2012
comment
Вы не видите, что magic появляется в вашей оболочке? Обычно я получаю None\nNone\nmagic\n, иногда два последних не по порядку, или 0 вместо второго None, но всегда за три строки до Lookup server. - person abarnert; 21.11.2012
comment
После внесения предложенных вами изменений, я делаю. Теперь я вижу намного больше того, что происходит. Спасибо за исчерпывающий ответ. Надеюсь, это перенесется в мою фактическую реализацию. - person jrbalsano; 21.11.2012
comment
И ключ к исправлению фактической программы, которую я запускал, заключался в том, чтобы добавить несколько снов. По сути, я не давал достаточно времени, чтобы ответить, прежде чем убить все процессы. - person jrbalsano; 21.11.2012
comment
@Redian: Это действительно не очень хорошее решение, если только вы не можете его избежать. Вы хотите правильно упорядочить вещи — например, с nc stdin.close гарантированно будет обработано после stdin.write, а затем гарантированно будет возвращен только wait в процессе точно в нужное время. Это может означать чтение из stdout, изменение способа запуска дочерних процессов и т. д., но если это вообще возможно, вы должны это сделать. В противном случае время от времени что-то (например, неожиданное зависание свопа или выход из спящего режима) сделает ваш sleep слишком коротким… - person abarnert; 21.11.2012

Ваш вызов nc неверен, что произойдет, если я вызову это как вы в командной строке :

# Server window:
[vyktor@grepfruit ~]$ nc -l 5050

# Client Window
[vyktor@grepfruit ~]$ nc localhost 5050
[vyktor@grepfruit ~]$ echo $?
1

Что означает (1 в $?) неудачу.

Как только вы используете -p:

-p, --local-port=NUM       local port number

NC начинает слушать, поэтому:

# Server window
[vyktor@grepfruit ~]$ nc -l -p 5050
    # Keeps handing

# Client window
[vyktor@grepfruit ~]$ echo Hi | nc localhost 5050
    # Keeps hanging

Как только вы добавите -c к вызову клиента:

-c, --close                close connection on EOF from stdin

Вы получите это:

# Client window
[vyktor@grepfruit ~]$ echo Hi | nc localhost 5050 -c
[vyktor@grepfruit ~]$

# Server window
[vyktor@grepfruit ~]$ nc -l -p 5050
Hi
[vyktor@grepfruit ~]$ 

Итак, вам нужен этот фрагмент кода Python:

#!/usr/bin/env python
import subprocess
lookup_server = subprocess.Popen("nc -l -p 5050", shell=True)
lookup_client = subprocess.Popen("nc -c localhost 5050", shell=True, 
                      stdin=subprocess.PIPE) 
lookup_client.stdin.write("magic\n")
lookup_client.stdin.close()            # This
lookup_client.send_signal(subprocess.signal.SIGINT) # or this kill
lookup_server.wait()
print "Lookup server terminated properly"
person Vyktor    schedule 20.11.2012
comment
Команды подходят для BSD netcat, но не для GNU netcat. Так что я предполагаю, что он на Mac или системе *BSD. - person abarnert; 21.11.2012
comment
@abarnert не сработало бы, если бы он был на BSD netcat? Это кажется мне наиболее вероятным объяснением ... Может быть, OP может добавить некоторые детали. - person Vyktor; 21.11.2012
comment
Нет, это не работает, потому что у него нет close(), и это единственная причина, по которой ваш работает с GNU netcat… - person abarnert; 21.11.2012
comment
Я только что установил GNU netcat на свой Mac, чтобы убедиться в этом — тогда вам нужно добавить аргументы -p и -c, и вы также получите дополнительный вывод stderr (read(net): Connection reset by peer), но больше ничего не изменилось по сравнению с BSD netcat — в частности, он все еще зависает с signal и отлично работает с close. - person abarnert; 21.11.2012
comment
@abarnert спасибо за усилия ... Раньше я не осознавал, что существует несколько разных реализаций. - person Vyktor; 21.11.2012
comment
На самом деле существует пять основных реализаций: оригинальный Hobbit, Win32, GNU, BSD и та, что поставляется с nmap. Поскольку почти все системы Linux поставляются с GNU, а OS X поставляется с BSD, и они несовместимы во всех случаях, кроме самых простых, это является серьезной головной болью совместимости (именно поэтому nmap включает свою собственную версию). - person abarnert; 21.11.2012