Uvicorn/FastAPI as a Windows service with cx_Freeze, cant stop service

Viewed 1471

Im pretty new to programming and Python and i have crated an API using FastAPI that i need to run on Windows, i want to run it as a service. The server will start and run fine, but when i try to stop it Windows throws an error and i think its because im not "stopping" uvicorn and i cant figure out how to. I've been googling and tried to run Uvicorn in a Daemon thread because i read that daemon threads will shutdown when the main thread exits, but that did not help either. Im converting the app to exe using cx_Freeze. Could anyone point me in the right direction?

Im using this Template for cx_Freeze: https://github.com/marcelotduarte/cx_Freeze/tree/main/cx_Freeze/samples/service

Im on Windows 10, Python 3.9.1, Uvicorn 0.13.3, cx_Freeze 6.5

ServiceHandler.py

import os
import sys
import cx_Logging
import api
import logging
import threading
from uvicorn import Config, Server


logging.basicConfig(
    filename = os.path.join(os.path.dirname(sys.executable), "log.txt"),
    level = logging.DEBUG, 
    format = '[API] %(levelname)-7.7s %(message)s'
)


class Handler:

    # no parameters are permitted; all configuration should be placed in the
    # configuration file and handled in the initialize() method
    def __init__(self):
        self.stopEvent = threading.Event()
        self.stopRequestedEvent = threading.Event()

    # called when the service is starting
    def initialize(self, configFileName):
        self.directory = os.path.dirname(sys.executable)
        cx_Logging.StartLogging(os.path.join(self.directory, "testing.log"), cx_Logging.DEBUG)
        #pass

    # called when the service is starting immediately after initialize()
    # use this to perform the work of the service; don't forget to set or check
    # for the stop event or the service GUI will not respond to requests to
    # stop the service
    def run(self):
        cx_Logging.Debug("stdout=%r", sys.stdout)
        sys.stdout = open(os.path.join(self.directory, "stdout.log"), "a")
        sys.stderr = open(os.path.join(self.directory, "stderr.log"), "a")
        self.main()
        self.stopRequestedEvent.wait()
        self.stopEvent.set()


    # called when the service is being stopped by the service manager GUI
    def stop(self):
        try:
            logging.debug("Stopping Service")
            self.stopRequestedEvent.set()
            self.stopEvent.wait()
            # How to stop the server???
        except Exception as e:
            logging.error(e)

    def main(self):
        try:
            self.config = Config(app=api.app, host="0.0.0.0", port=8004, reload=False)
            self.app_server = Server(self.config)
            self.app_server.install_signal_handlers = lambda: None # Need this line, or the server wont start
            self.app_server.run()
        except Exception as e:
            logging.error(e)

setup.py

from cx_Freeze import setup, Executable

options = {
    "build_exe": {
        "packages": ["uvicorn", "fastapi", "pydantic", "threading"],
        "includes": ["ServiceHandler", "cx_Logging", "ipaddress", "colorsys"],
        "excludes": ["tkinter"],
    }
}

executables = [
    Executable(
        "Config.py",
        base="Win32Service",
        target_name="api.exe",
    )
]

setup(
    name="TestService",
    version="0.1",
    description="Sample Windows serice",
    executables=executables,
    options=options,
)

I've also read this; https://github.com/encode/uvicorn/issues/742 But my knowledge is limited, so i dont quite understand how to implement it in my app?

1 Answers

So i got the service working. Uvicorn/FastAPI will now start and stop as a Windows service, i dont know if there are any potential drawbacks, but here is the working code;

import os
import sys
import cx_Logging
import api
import logging
import threading
from uvicorn import Config, Server


logging.basicConfig(
    filename = os.path.join(os.path.dirname(sys.executable), "log.txt"),
    level = logging.DEBUG, 
    format = '[API] %(levelname)-7.7s %(message)s'
)

#My class for creating and running Uvicorn in a thread
class AppServer:
    def __init__(self, app, host: str = "0.0.0.0", port: int = 8004, reload: bool = False):
        self.config = Config(app=app, host=host, port=port, reload=reload)
        self.server = Server(self.config)
        self.server.install_signal_handlers = lambda: None # Need this line, or the server wont start
        self.proc = None
    
    def run(self):
        self.server.run()

    def start(self):
        self.proc = threading.Thread(target=self.run, name="Test", args=())
        self.proc.setDaemon(True)
        self.proc.start()

    def stop(self):
        if self.proc:
            self.proc.join(0.25)


class Handler:

    # no parameters are permitted; all configuration should be placed in the
    # configuration file and handled in the initialize() method
    def __init__(self):
        self.stopEvent = threading.Event()
        self.stopRequestedEvent = threading.Event()

    # called when the service is starting
    def initialize(self, configFileName):
        self.directory = os.path.dirname(sys.executable)
        cx_Logging.StartLogging(os.path.join(self.directory, "testing.log"), cx_Logging.DEBUG)
        #pass

    # called when the service is starting immediately after initialize()
    # use this to perform the work of the service; don't forget to set or check
    # for the stop event or the service GUI will not respond to requests to
    # stop the service
    def run(self):
        cx_Logging.Debug("stdout=%r", sys.stdout)
        sys.stdout = open(os.path.join(self.directory, "stdout.log"), "a")
        sys.stderr = open(os.path.join(self.directory, "stderr.log"), "a")
        self.main()
        self.stopRequestedEvent.wait()
        self.stopEvent.set()


    # called when the service is being stopped by the service manager GUI
    def stop(self):
        try:
            logging.debug("Stopping Service")
            self.server.stop()
            self.stopRequestedEvent.set()
            self.stopEvent.wait()
            # How to stop the server???
        except Exception as e:
            logging.error(e)


    def main(self):
        try:
            logging.debug("Starting server,,,")
            self.server = AppServer(app=api.app)
            self.server.start()
        except Exception as e:
            logging.error(e)

I got the answer here; How to use FastAPI and uvicorn.run without blocking the thread?

Related