CTRL-C ведет себя по-разному в Python - PullRequest
3 голосов
/ 03 ноября 2010

Я недавно начал изучать Python (здесь долгое время работал Java-программистом) и в настоящее время пишу несколько простых серверных программ. Проблема заключается в том, что для, казалось бы, похожего фрагмента кода, аналог Java должным образом реагирует на сигнал SIGINT ( Ctrl + C ), а Python - нет. Это видно, когда для порождения сервера используется отдельный поток. Код выглядит следующим образом:

// Java code

package pkg;

import java.io.*;
import java.net.*;

public class ServerTest {

    public static void main(final String[] args) throws Exception {
        final Thread t = new Server();
        t.start();
    }

}

class Server extends Thread {

    @Override
    public void run() {
        try {
            final ServerSocket sock = new ServerSocket(12345);
            while(true) {
                final Socket clientSock = sock.accept();
                clientSock.close();
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

}

и код Python:

# Python code
import threading, sys, socket, signal

def startserver():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 12345))
    s.listen(1)
    while True:
        csock, caddr = s.accept()
        csock.sendall('Get off my lawn kids...\n')
        csock.close()

if __name__ == '__main__':
    try:
        t = threading.Thread(target=startserver)
        t.start()
    except:
        sys.exit(1)

В обоих приведенных выше фрагментах кода я создаю простой сервер, который прослушивает TCP-запросы для данного порта. Когда в случае кода Java нажимается Ctrl + C , JVM завершает работу, тогда как в случае кода Python все, что я получаю, это ^C в оболочке. Единственный способ остановить сервер - это нажать Ctrl + Z и затем вручную завершить процесс.

Так что я придумаю план; почему бы не иметь sighandler, который слушает Ctrl + Z и выходит из приложения? Круто, вот я и придумала:

import threading, sys, socket, signal

def startserver():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 12345))
    s.listen(1)
    while True:
        csock, caddr = s.accept()
        csock.sendall('Get off my lawn kids...\n')
        csock.close()

def ctrlz_handler(*args, **kwargs):
    sys.exit(1)

if __name__ == '__main__':
    try:
        signal.signal(signal.SIGTSTP, ctrlz_handler)
        t = threading.Thread(target=startserver)
        t.start()
    except:
        sys.exit(1)

Но теперь, похоже, я еще больше усугубил ситуацию! Теперь нажатие Ctrl + C на оболочке создает '^ C', а нажатие Ctrl + Z на оболочке - ^ Z ' ,

Итак, мой вопрос: почему это странное поведение? Вероятно, у меня будет несколько серверных процессов, запущенных как отдельные потоки в одном и том же процессе, поэтому любой простой способ убить сервер, когда процесс получит SIGINT? Кстати, используя Python 2.6.4 в Ubuntu.

ТИА
Sasuke

Ответы [ 2 ]

5 голосов
/ 03 ноября 2010

Несколько проблем:

  • Не перехватывать все исключения и молча выходить.Это самое худшее, что вы можете сделать.

  • Если вы действительно не знаете, что делаете, никогда не поймайте SIGTSTP.Это не сигнал, предназначенный для перехвата приложений, и если вы его ловите, скорее всего, вы делаете что-то не так.

  • KeyboardInterrupt отправляется только в начальный поток программыи никогда в другие темы.Это сделано по нескольким причинам.Как правило, вы хотите иметь дело с ^ C в одном месте, а не в случайном, произвольном потоке, который получает исключение.Кроме того, это помогает гарантировать, что при нормальной работе большинство потоков никогда не получат асинхронные исключения;это полезно, поскольку (на всех языках, включая Java) с ними очень трудно надежно работать.

  • Вы никогда не увидите здесь KeyboardInterrupt, потому что ваш основной поток завершает работусразу после запуска вторичного потока.Нет основного потока, чтобы получить исключение, и оно просто отбрасывается.

Исправление состоит из двух частей: держите основной поток вокруг, чтобы KeyboardInterrupt было куда идти, и только перехватKeyboardInterrupt, не все исключения.


import threading, sys, socket, signal, time

def startserver():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 12345))
    s.listen(1)
    while True:
        csock, caddr = s.accept()
        csock.sendall('Get off my lawn kids...\n')
        csock.close()

if __name__ == '__main__':
    try:
        t = threading.Thread(target=startserver)
        t.start()

        # Wait forever, so we can receive KeyboardInterrupt.
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print "^C received"
        sys.exit(1)

    # We never get here.
    raise RuntimeError, "not reached"
1 голос
/ 03 ноября 2010
  1. созданный вами поток не является демоническим, поэтому он не завершается при выходе из родительского потока;

  2. основной выход сразу после запуска дочернего потокаthread, процесс python ожидает завершения дочернего потока;

  3. offtop: не забудьте закрыть сокеты;

  4. вы должны реализоватькакой-то механизм остановки для дочерней нити.Простое решение: установите флажок stopped в цикле while, который принимает клиентские подключения (используйте уровень модуля или расширяйте класс threading.Thread для его инкапсуляции), установите этот флаг в True и отключите сокет сервера (подключив) по Ctrl + C и / или Ctrl + Z или без них;подождите в главном блоке while t.isAlive(): time.sleep(1).Вот пример кода:


import threading, sys, socket, signal, time

class Server(threading.Thread):
  def init(self):
    threading.Thread.init(self)
    self._stop = False
    self._port =12345

def stop(self):
    self.__stop = True
    self.kick()

def kick(self):
    try:
      s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      s.connect(('', self.__port))
      s.close()
    except IOError, e:
      print "Failed to kick the server:", e

def run(self):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', self._port))
    s.listen(1)
    try:
      while True:
        csock, caddr = s.accept()
        if self._stop:
          print "Stopped, breaking"
          break
        print "client connected from", caddr
        try:
          csock.sendall('Get off my lawn kids...\n')
        finally:
          csock.close()
    finally:
      s.close()

if name == 'main':
  t = Server()
  # if it is failed to stop the thread properly, it will exit 
  t.setDaemon(True)
  t.start()
  try:
    while t.isAlive():
      time.sleep(1)
  except: # Ctrl+C brings us here
    print "Maybe, you have interrupted this thing?"
  t.stop()
  # t.join() may be called here, or another synchronization should appear to
  # make sure the server stopped correctly

Я не знаю, почему он показывает методы stop, kick, run как уровень модуля и ставит более одногопереводы строк, извините

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...