13

I have managed to cobble together a working demo of a pywin32 Windows service running Flask inside the Pylons waitress WSGI server (below). A niece self contained solution is the idea...

I have spent hours reviewing and testing ways of making waitress exit cleanly (like this and this), but the best I can do so far is a kind of suicidal SIGINT which makes Windows complain "the pipe has been ended" when stopping through the Services control panel, but at least it stops :-/ I guess the pythonservice.exe which pywin32 starts, should not terminate, just the waitress treads?

To be honest I'm still uncertain if this is a question about waitress, pywin32, or maybe it's just plain Python. I do have the feeling the answer is right in front of me, but right now I'm completely stumped.

import os
import random
import signal
import socket

from flask import Flask, escape, request

import servicemanager
import win32event
import win32service
import win32serviceutil
from waitress import serve

app = Flask(__name__)


@app.route('/')
def hello():
    random.seed()
    x = random.randint(1, 1000000)
    name = request.args.get("name", "World")
    return 'Hello, %s! - %s - %s' % (escape(name), x, os.getpid())


# based on https://www.thepythoncorner.com/2018/08/how-to-create-a-windows-service-in-python/

class SMWinservice(win32serviceutil.ServiceFramework):
    '''Base class to create winservice in Python'''

    _svc_name_ = 'WaitressService'
    _svc_display_name_ = 'Waitress server'
    _svc_description_ = 'Python waitress WSGI service'

    @classmethod
    def parse_command_line(cls):
        '''
        ClassMethod to parse the command line
        '''
        win32serviceutil.HandleCommandLine(cls)

    def __init__(self, args):
        '''
        Constructor of the winservice
        '''
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
        socket.setdefaulttimeout(60)

    def SvcStop(self):
        '''
        Called when the service is asked to stop
        '''
        self.stop()
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
                            servicemanager.PYS_SERVICE_STOPPED,
                            (self._svc_name_, ''))
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.hWaitStop)

    def SvcDoRun(self):
        '''
        Called when the service is asked to start
        '''
        self.start()
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
                            servicemanager.PYS_SERVICE_STARTED,
                            (self._svc_name_, ''))
        self.main()

    def start(self):
        pass

    def stop(self):
        print 'sigint'
        os.kill(os.getpid(), signal.SIGINT)

    def main(self):
        print 'serve'
        serve(app, listen='*:5000')


if __name__ == '__main__':
    SMWinservice.parse_command_line()
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alias_Knagg
  • 886
  • 1
  • 7
  • 21

1 Answers1

9

I have found a solution using a sub-thread that seems to work. I am not quite sure if this may have possible unintended consequences yet...

I believe the updated version below, "injecting" a SystemExit into the waitress thread is as good as it gets. I think thee original kills the thread hard, but this one prints "thread done" indicating a graceful shutdown.

Corrections or improvements welcome!

import ctypes
import os
import random
import socket
import threading

from flask import Flask, escape, request

import servicemanager
import win32event
import win32service
import win32serviceutil
from waitress import serve

app = Flask(__name__)

# waitress thread exit based on:
# https://www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/

@app.route('/')
def hello():
    random.seed()
    x = random.randint(1, 1000000)
    name = request.args.get("name", "World")
    return 'Hello, %s! - %s - %s' % (escape(name), x, os.getpid())


class ServerThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        print('thread start\n')
        serve(app, listen='*:5000')  # blocking
        print('thread done\n')

    def get_id(self):
        # returns id of the respective thread
        if hasattr(self, '_thread_id'):
            return self._thread_id
        for id, thread in threading._active.items():
            if thread is self:
                return id

    def exit(self):
        thread_id = self.get_id()
        res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit))
        if res > 1:
            ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
            print('Exception raise failure')


class SMWinservice(win32serviceutil.ServiceFramework):
    _svc_name_ = 'WaitressService'
    _svc_display_name_ = 'Waitress server'
    _svc_description_ = 'Python waitress WSGI service'

    @classmethod
    def parse_command_line(cls):
        win32serviceutil.HandleCommandLine(cls)

    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.stopEvt = win32event.CreateEvent(None, 0, 0, None)
        socket.setdefaulttimeout(60)

    def SvcStop(self):
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
                            servicemanager.PYS_SERVICE_STOPPED,
                            (self._svc_name_, ''))
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.stopEvt)

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

    def main(self):
        print('main start')
        self.server = ServerThread()
        self.server.start()
        print('waiting on win32event')
        win32event.WaitForSingleObject(self.stopEvt, win32event.INFINITE)
        self.server.exit()  # raise SystemExit in inner thread
        print('waiting on thread')
        self.server.join()
        print('main done')


if __name__ == '__main__':
    SMWinservice.parse_command_line()
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alias_Knagg
  • 886
  • 1
  • 7
  • 21
  • How do you use this script? – DiamondDrake Jun 01 '20 at 17:43
  • It's a minimal standalone Flask app. Just try it out. The SMWinservice CLI has build-in help. You can import the app var if you already have one. – Alias_Knagg Jun 03 '20 at 05:26
  • 1
    I’m using the same script for a Django app (ver 3.1) – elsadek Jun 27 '21 at 13:33
  • I'm not sure if this of some help for you, but I stumbled on the same problem with stopping waitress. For me it seems to be enough to call "self.ReportServiceStatus(win32service.SERVICE_STOPPED)" inside the SvcDoStop method, while the app was run just by "serve(...)" inside the main method. Not sure if this is bad practice, but seems to do the trick for me. – Nick Nov 08 '21 at 16:31
  • Thanks for the suggestion. It would be good to eliminate the "inner thread", but most of the examples of "self.ReportServiceStatus(win32service.SERVICE_STOPPED)" does some kind of "own stop" first, so I assume that's how it's intended. Also just reporting status to be stopped making it so, seems strange on the face of it. I will try your suggestion when I have the opportunity, to see if I can figure out how it works, I hardly have this issue at my fingertips at the moment :-) – Alias_Knagg Nov 08 '21 at 21:52
  • I'm readying to try this solution myself but wanted to share: Based on a brief dig into the waitress source code, it appears to me it handles `KeyboardInterrupt` and `SystemExit` exceptions in exactly the same manner. I found 3 places where `KeyboardInterrupt` is used (2 in an `except`, one in a tuple definition), and in all 3 the same line includes `SystemExit` as well. So this bolsters the idea this is pretty clean solution. – JDM May 10 '23 at 14:17
  • Followup: The solution is working perfectly with my mssql-django server (Django 4.1.9). – JDM May 10 '23 at 15:03