Python - как перезапустить приложение "на лету", когда приложение имеет порт TCP в режиме прослушивания?

Каков наилучший способ перезапуска приложения, в котором он запускал TCP-порт прослушивания? Проблема заключается в следующем: если я быстро запустил приложение в качестве перезапуска, он терпит неудачу, потому что прослушиваемый сокет уже используется.

Как безопасно перезапустить в таком случае?

socket.error: [Errno 98] Address already in use

код:

#!/usr/bin/python
import sys,os
import pygtk, gtk, gobject
import socket, datetime, threading
import ConfigParser
import urllib2
import subprocess

def server(host, port):
  sock = socket.socket()
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  sock.bind((host, port))
  sock.listen(1)
  print "Listening... " 
  gobject.io_add_watch(sock, gobject.IO_IN, listener)


def listener(sock, *args):
  conn, addr = sock.accept()
  print "Connected"
  gobject.io_add_watch(conn, gobject.IO_IN, handler)
  return True

def handler(conn, *args):
  line = conn.recv(4096)
  if not len(line):
    print "Connection closed."
    return False
  else:
    print line
    if line.startswith("unittest"):
      subprocess.call("/var/tmp/runme.sh", shell=True)
    else:
      print "not ok"
  return True

server('localhost', 8080)
gobject.MainLoop().run()

runme.sh

#!/bin/bash
ps aux | grep py.py | awk '{print $2}' | xargs kill -9;
export DISPLAY=:0.0 && lsof -i tcp:58888 | grep LISTEN | awk '{print $2}' | xargs kill -9;
export DISPLAY=:0.0 && java -cp Something.jar System.V &
export DISPLAY=:0.0 && /var/tmp/py.py &

EDIT:  Обратите внимание, что я использую Java и Python вместе как одно приложение с двумя слоями. Итак, runme.sh - это мой запуск script для запуска обоих приложений одновременно. Из Java я нажимаю кнопку перезапуска Python. Но Python не перезапускает, потому что kill выполняется через BASH.

Ответ 1

Вам нужно будет найти эквивалент Python для установки SO_REUSEADDR в сокете, прежде чем связывать его. Обеспечение закрытия розетки при выходе, как рекомендовано в других ответах, не является ни необходимым, ни достаточным, так как (а) сокеты закрываются ОС при выходе из процесса и (б) вам все равно придется преодолевать принятые соединения в состоянии TIME_WAIT, который может выполнять только SO_REUSEADDR.

Ответ 2

1.

У вас есть проблема с убийством вашего python

air:~ dima$ ps aux | grep i-dont-exist.py | awk '{print $2}'
34198

Это означает, что ваш процесс grep попадает в вашу логику перезапуска и убивается.

В linux вы можете использовать pidof вместо этого.

Альтернативно используйте start-stop-daemon и pid файл.

2.

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

Для быстрого теста добавьте спать, прежде чем запускать python снова.

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

Ответ 3

Есть ли вероятность, что ваша программа Python запускает другие процессы? например через fork, subprocess или os.system?

Возможно, что ваш дескриптор прослушивающего файла наследуется порожденным процессом:

os.system( "sleep 1000" ) # без сокетов:

ls -l /proc/`pidof sleep`/fd
total 0
lrwx------ 1 user user 64 2012-12-19 19:52 0 -> /dev/pts/0
lrwx------ 1 user user 64 2012-12-19 19:52 1 -> /dev/pts/0
l-wx------ 1 user user 64 2012-12-19 19:52 13 -> /dev/null
lrwx------ 1 user user 64 2012-12-19 19:52 2 -> /dev/pts/0

разъем(); setsockopt(); связывания(); Слушать(); os.system( "sleep 1000" ) # с сокетами:

ls -l /proc/`pidof sleep`/fd
total 0
lrwx------ 1 user user 64 2012-12-19 19:49 0 -> /dev/pts/0
lrwx------ 1 user user 64 2012-12-19 19:49 1 -> /dev/pts/0
l-wx------ 1 user user 64 2012-12-19 19:49 13 -> /dev/null
lrwx------ 1 user user 64 2012-12-19 19:49 2 -> /dev/pts/0
lrwx------ 1 user user 64 2012-12-19 19:49 5 -> socket:[238967]
lrwx------ 1 user user 64 2012-12-19 19:49 6 -> socket:[238969]

Возможно, ваш Python script умер, но его дети этого не сделали, последние сохраняют ссылку на прослушивающий сокет, и поэтому новый процесс Python не может связываться с одним и тем же адресом.

Ответ 4

Вот мое предположение: kill асинхронно. Он просто сообщает ядру отправить сигнал процессу, он также не дожидается, когда сигнал будет доставлен и обработан. Перед перезапуском процесса вы должны использовать команду wait.

$ wait $PID

Ответ 5

Вы можете добавить больше логики к вашему запуску script, чтобы выполнить предварительное тестирование и очистку.

#!/bin/bash
export DISPLAY=:0.0

# If py.py is found running
if pgrep py.py; then
 for n in $(seq 1 9); do
  # kill py.py starting at kill -1 and increase to kill -9
  if ! pgrep py.py; then
   # if no running py.py is found break out of this loop
   break
  fi
  pkill -${n} py.py
  sleep .5
 done
fi

# Verify nothing has tcp/58888 open in a listening state
if lsof -t -i tcp:58888 -stcp:listen; then
 echo process with pid $(lsof -t -i tcp:58888 -stcp:listen) still listening on port 58888, exiting
 exit
fi

java -cp Something.jar System.V &
/var/tmp/py.py &

В конце концов вы, вероятно, захотите использовать полномасштабный init script и демонзировать эти процессы. См. http://www.thegeekstuff.com/2012/03/lsbinit-script/ для примера, хотя, если ваши процессы запущены как непривилегированный пользователь, который немного изменит реализацию, но общие концепции то же самое.

Ответ 6

Возможное решение # 1: Fork и выполнить новую копию вашего python script из старой. Он унаследует прослушивающий сокет. Затем, при желании, отсоедините его от родителя и убейте (или выйдите) из родителя. Обратите внимание, что родительский (старая версия) может завершить обслуживание любых существующих запросов, даже если ребенок (новая версия) обрабатывает любые новые входящие запросы.

Возможное решение №2: передать старый запуск script передать сокет новому script с sendmsg() и SCM_RIGHTS, а затем убить старый script. Этот пример кода говорит о "дескрипторах файлов", но отлично работает с сокетами. Смотрите: Как передать прослушивающий сокет TCP с минимальным временем простоя?

Возможное решение # 3: Если bind() возвращает EADDRINUSE, подождите некоторое время и повторите попытку, пока он не удастся. Если вам нужно перезапустить script быстро и без простоя между ними, это не сработает, конечно:)

Возможное решение №4: Не убивайте свой процесс с помощью kill -9. Убейте его другим сигналом, например SIGTERM. Поймайте SIGTERM и вызовите gobject.MainLoop.quit(), когда получите это.

Возможное решение №5: Убедитесь, что на нем установлен родительский процесс вашего python script (например, оболочка) wait. Если родительский процесс script не запущен, или если script демонанизирован, то, если его убили с помощью SIGKILL, init станет его родителем. init вызывает wait периодически, но это может занять немного времени, возможно, это то, с чем вы столкнулись. Если вы должны использовать SIGKILL, но вы хотите, чтобы более быстрая очистка просто вызывала wait самостоятельно.

Решения 4 и 5 имеют очень короткое, но отличное от нуля время между остановкой старого script и запуском нового. Решение 3 имеет потенциально значительное время между ними, но очень просто. Решения 1 и 2 - это способы сделать это буквально без простоя: любой вызов соединения будет успешным и получит либо старый, либо новый запуск script.

P.S. Более подробно о поведении SO_REUSEADDR на разных платформах: SO_REUSEADDR не имеет той же семантики в Windows, что и в Unix

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

Я не уверен, что это то, над чем вы работаете, но обратите внимание, что, как описано здесь, поведение в разных версиях Unix также несколько отличается.

Ответ 7

Что бы я ни пытался, не работал. Поэтому, чтобы уменьшить риск, я начал использовать файловую систему в качестве примера сокета:

# Echo server program
import socket,os

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
    os.remove("/tmp/socketname")
except OSError:
    pass
s.bind("/tmp/socketname")
s.listen(1)
conn, addr = s.accept()
while 1:
    data = conn.recv(1024)
    if not data: break
    conn.send(data)
conn.close()


# Echo client program
import socket

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect("/tmp/socketname")
s.send('Hello, world')
data = s.recv(1024)
s.close()
print 'Received', repr(data)