Изящная остановка гринлетов в службе Windows

Используя pywin32 и gevent, я создаю службу Windows, которая выполняет две функции:

  1. Он запускает веб-сервер для простого веб-приложения (используя адаптер сервера gevent для бутылки, который запускает serve_forever() WSGIServer).
  2. Он прослушивает входящие SIP-вызовы (используя SIP-клиент на основе gevent) и запускает простой код для ответа на вызовы.

Я хочу, чтобы служба просто поддерживала работу веб-сервера и SIP-клиента вечно, но но останавливалась немедленно и корректно, если я попытаюсь остановить службу Windows. Кажется, это должно быть довольно просто.

То, что я сейчас делаю для запуска приложения, в основном запускает веб-сервер и SIP-клиент каждый в гринлете и запускает kill на обоих гринлетах, когда я хочу остановить приложение (упрощенный макет):

from bottle import run
from mysipclient import SipClient
import gevent

def sip_listen():
    client = SipClient()
    try:
        client.wait()  # This method blocks on a gevent queue.get call
    finally:
        client.close()  # This does some cleanup, like deregistering from the SIP server, that I really want to run when the service stops!

class App(object):
    def start(self):
        self.stop_event = gevent.event.Event()
        self.server_greenlet = gevent.spawn(run, server='gevent', host='0.0.0.0', port=8080)
        self.sip_greenlet = gevent.spawn(sip_listen)

        gevent.joinall([self.server_greenlet, self.sip_greenlet])

    def stop(self):
        self.server_greenlet.kill()
        self.sip_greenlet.kill()

if __name__ == "__main__":
    app = App()
    gevent.spawn_later(10, app.stop)
    app.start()

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

Однако теперь я пытаюсь превратить это в службу Windows, используя win32serviceutil pywin32:

import win32serviceutil
import win32service
import win32event
import servicemanager
from app import App

class TestService(win32serviceutil.ServiceFramework):
    _svc_name_ = 'TestService'
    _svc_display_name_ = 'TestService'

    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.service_obj = App()

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        self.service_obj.stop()

    def SvcDoRun(self):
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
        self.service_obj.start()

if __name__ == '__main__':
    if len(sys.argv) == 1:
        servicemanager.Initialize()
        servicemanager.PrepareToHostSingle(TestService)
        servicemanager.StartServiceCtrlDispatcher()
    else:
        win32serviceutil.HandleCommandLine(TestService)

Когда я устанавливаю это как службу и запускаю ее, я получаю исключение при попытке убить гринлеты: LoopExit: This operation would block forever. Затем служба не останавливается, и мне приходится убивать ее вручную. (Я могу избежать этого, перехватив исключение, используя событие вместо присоединения к гринлетам и установив событие на остановку, но это означает, что очистка вообще не выполняется.)

Я новичок как в gevent, так и в работе со службами Windows, и Google не очень помог. Я подумал, может быть, разница была в том, как с версией командной строки я запускал стоп в другом гринлете, поэтому я попытался заменить self.service_obj.stop() в SvcStop на gevent.spawn(self.service_obj.stop).join(), но таким образом он даже не выдает исключение и просто полностью зависает, пока я не убью процесс.

Что тут происходит? Я делаю что-то принципиально неправильное? Как изящно остановить гринлеты на SvcStop?


person antialiasis    schedule 06.08.2019    source источник


Ответы (1)


Вам нужно поймать сигнальное событие

from gevent import signal

def shutdown():
    print('Shutting down ...')
    server.stop(timeout=60)
    exit(signal.SIGTERM)
gevent.signal(signal.SIGTERM, shutdown)
gevent.signal(signal.SIGQUIT, shutdown)
gevent.signal(signal.SIGINT, shutdown) #CTRL C
... start server code 
person eatmeimadanish    schedule 06.08.2019
comment
Спасибо за ответ, но не могли бы вы уточнить, как мне следует использовать это в этом случае? При кратком тестировании с обработчиком сигналов, который просто записывает в файл журнала, он запускается нормально, когда я нажимаю Ctrl-C в командной строке, но вообще не запускается, когда я останавливаю службу Windows. - person antialiasis; 07.08.2019
comment
Я добавил еще один тип сигнала. Они должны ловить каждый раз, когда ОС отправляет процессу команду kill. - person eatmeimadanish; 07.08.2019
comment
SIGQUIT не существует в Windows. - person antialiasis; 07.08.2019
comment
SIGTERM должен обрабатывать эти случаи. Возможно, вам придется добавить gevent.kill, чтобы убить все потоки в вашем обратном вызове. - person eatmeimadanish; 08.08.2019