├── app ├── __init__.py ├── routers │ ├── __init__.py │ └── items.py └── main.py ├── scripts ├── debug.bat ├── start.bat ├── remove.bat ├── install.bat └── build.bat ├── .gitignore ├── .env ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── config.yml ├── README.md ├── .hooks ├── hook-fastapi.py └── hook-uvicorn.py ├── app-service.spec ├── app-console.spec ├── main.py └── service.py /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/debug.bat: -------------------------------------------------------------------------------- 1 | app-service.exe debug -------------------------------------------------------------------------------- /scripts/start.bat: -------------------------------------------------------------------------------- 1 | app-service.exe start -------------------------------------------------------------------------------- /scripts/remove.bat: -------------------------------------------------------------------------------- 1 | app-service.exe remove -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | logs/ 4 | __pycache__ -------------------------------------------------------------------------------- /scripts/install.bat: -------------------------------------------------------------------------------- 1 | app-service.exe --startup auto install -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbeaugrand-org/PythonFastApi/HEAD/.env -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "C:\\Users\\beaugrandk\\Miniconda3\\envs\\dev\\python.exe" 3 | } -------------------------------------------------------------------------------- /app/routers/items.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | 3 | router = APIRouter() 4 | 5 | @router.get("/") 6 | async def get_items(): 7 | return [{"name": "Item Foo"}, {"name": "item Bar"}] -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | server: 2 | log: 3 | level: debug 4 | access_log: false 5 | format: '%(asctime)-15s %(levelname)s %(message)s' 6 | file: 7 | path: logs/web.log 8 | maxBytes: 2000 9 | keep: 10 10 | host: 127.0.0.1 11 | port: 5000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows Service in Python 2 | 3 | ## How To Build the service 4 | 5 | ``` 6 | pyinstaller -F --hidden-import=win32timezone WindowsService.py 7 | ``` 8 | 9 | ## How To install the service 10 | 11 | ``` 12 | .\dist\WindowsService.exe --startup auto install 13 | ``` -------------------------------------------------------------------------------- /scripts/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | pyinstaller app-console.spec 4 | pyinstaller app-service.spec 5 | 6 | COPY .\scripts\install.bat .\dist\ 7 | COPY .\scripts\debug.bat .\dist\ 8 | COPY .\scripts\start.bat .\dist\ 9 | COPY .\scripts\remove.bat .\dist\ 10 | 11 | COPY .\scripts\config.yml .\dist\ -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | For more information about how to create FastApis, go to: https://fastapi.tiangolo.com/#create-it 3 | """ 4 | from fastapi import Depends, FastAPI, Header, HTTPException 5 | 6 | from .routers import items 7 | 8 | api = FastAPI() 9 | 10 | api.include_router( 11 | items.router, 12 | prefix="/items", 13 | tags=["items"], 14 | responses={404: {"description": "Not found"}}, 15 | ) -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build", 8 | "type": "shell", 9 | "command": ".\\scripts\\build.bat", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/main.py", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.hooks/hook-fastapi.py: -------------------------------------------------------------------------------- 1 | hiddenimports = [ 2 | 'fastapi', 3 | 'fastapi.applications', 4 | 'fastapi.datastructures', 5 | 'fastapi.dependencies', 6 | 'fastapi.dependencies.models', 7 | 'fastapi.dependencies.utils', 8 | 'fastapi.encoders', 9 | 'fastapi.exception_handlers', 10 | 'fastapi.exceptions', 11 | 'fastapi.openapi', 12 | 'fastapi.openapi.constants', 13 | 'fastapi.openapi.docs', 14 | 'fastapi.openapi.models', 15 | 'fastapi.openapi.utils', 16 | 'fastapi.param_functions', 17 | 'fastapi.params', 18 | 'fastapi.routing', 19 | 'fastapi.security', 20 | 'fastapi.security.api_key', 21 | 'fastapi.security.base', 22 | 'fastapi.security.http', 23 | 'fastapi.security.oauth2', 24 | 'fastapi.security.open_id_connect_url', 25 | 'fastapi.security.utils', 26 | 'fastapi.utils', 27 | ] -------------------------------------------------------------------------------- /app-service.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['service.py'], 7 | pathex=['C:\\Users\\beaugrandk\\source\\repos\\python_service'], 8 | binaries=[], 9 | datas=[], 10 | hiddenimports=['win32timezone'], 11 | hookspath=[], 12 | runtime_hooks=[], 13 | excludes=[], 14 | win_no_prefer_redirects=False, 15 | win_private_assemblies=False, 16 | cipher=block_cipher, 17 | noarchive=False) 18 | pyz = PYZ(a.pure, a.zipped_data, 19 | cipher=block_cipher) 20 | exe = EXE(pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.zipfiles, 24 | a.datas, 25 | [], 26 | name='app-service', 27 | debug=False, 28 | bootloader_ignore_signals=False, 29 | strip=False, 30 | upx=True, 31 | runtime_tmpdir=None, 32 | console=True ) 33 | -------------------------------------------------------------------------------- /app-console.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['main.py'], 7 | pathex=['C:\\Users\\beaugrandk\\source\\repos\\python_service'], 8 | binaries=[], 9 | datas=[], 10 | hiddenimports=['win32timezone'], 11 | hookspath=['.hooks'], 12 | runtime_hooks=[], 13 | excludes=[], 14 | win_no_prefer_redirects=False, 15 | win_private_assemblies=False, 16 | cipher=block_cipher, 17 | noarchive=False) 18 | pyz = PYZ(a.pure, a.zipped_data, 19 | cipher=block_cipher) 20 | exe = EXE(pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.zipfiles, 24 | a.datas, 25 | [], 26 | name='app-console', 27 | debug=False, 28 | bootloader_ignore_signals=False, 29 | strip=False, 30 | upx=True, 31 | runtime_tmpdir=None, 32 | console=True ) 33 | -------------------------------------------------------------------------------- /.hooks/hook-uvicorn.py: -------------------------------------------------------------------------------- 1 | hiddenimports = [ 2 | 'uvicorn.config', 3 | 'uvicorn.importer', 4 | 'uvicorn.lifespan', 5 | 'uvicorn.lifespan.on', 6 | 'uvicorn.lifespan.off', 7 | 'uvicorn.loops', 8 | 'uvicorn.loops.asyncio', 9 | 'uvicorn.loops.auto', 10 | 'uvicorn.loops.uvloop', 11 | 'uvicorn.main', 12 | 'uvicorn.middleware', 13 | 'uvicorn.middleware.asgi2', 14 | 'uvicorn.middleware.debug', 15 | 'uvicorn.middleware.message_logger', 16 | 'uvicorn.middleware.proxy_headers', 17 | 'uvicorn.middleware.wsgi', 18 | 'uvicorn.protocols', 19 | 'uvicorn.protocols.http', 20 | 'uvicorn.protocols.http.auto', 21 | 'uvicorn.protocols.http.h11_impl', 22 | 'uvicorn.protocols.http.httptools_impl', 23 | 'uvicorn.protocols.utils', 24 | 'uvicorn.protocols.websockets', 25 | 'uvicorn.protocols.websockets.auto', 26 | 'uvicorn.protocols.websockets.websockets_impl', 27 | 'uvicorn.protocols.websockets.wsproto_impl', 28 | 'uvicorn.supervisors', 29 | 'uvicorn.supervisors.multiprocess', 30 | 'uvicorn.supervisors.statreload', 31 | 'uvicorn.workers'] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import yaml 4 | 5 | import logging 6 | from logging.handlers import RotatingFileHandler 7 | 8 | import uvicorn 9 | 10 | from app import main 11 | 12 | config_name = "config.yml" 13 | 14 | # determine if application is a script file or frozen exe 15 | if getattr(sys, 'frozen', False): 16 | application_path = os.path.dirname(sys.executable) 17 | elif __file__: 18 | application_path = os.path.dirname(__file__) 19 | 20 | config_path = os.path.join(application_path, config_name) 21 | 22 | with open(config_path, 'r') as stream: 23 | config = yaml.safe_load(stream) 24 | 25 | logging.basicConfig(filename=config['server']['log']['file']['path'], 26 | format=config['server']['log']['format'], 27 | level=config['server']['log']['level'].upper()) 28 | 29 | logger = logging.getLogger('main_logger') 30 | handler = RotatingFileHandler(config['server']['log']['file']['path'], 31 | maxBytes=config['server']['log']['file']['maxBytes'], 32 | backupCount=config['server']['log']['file']['keep']) 33 | logger.addHandler(handler) 34 | 35 | uvicorn.run(main.api, 36 | host=config['server']['host'], 37 | port=config['server']['port'], 38 | log_level=config['server']['log']['level'], 39 | access_log=config['server']['log']['access_log'], 40 | logger=logger, 41 | reload=False) -------------------------------------------------------------------------------- /service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of a Windows service implemented in Python. 3 | 4 | This module implements a simple Windows service, and can be invoked as 5 | a script with various arguments to install, start, stop, update, remove, 6 | etc. the service. The script (but not the service) must run with 7 | administrator privileges. 8 | 9 | To run a Windows command prompt that has administrator privileges, 10 | right-click on it in a Windows Explorer window (the path of the program 11 | is C:\Windows\System32\cmd.exe) and select "Run as administrator". Or, if 12 | you have installed the command prompt in the Windows taskbar, right click 13 | on its icon, right-click on "Command Prompt" in the resulting menu, and 14 | select "Run as administrator". 15 | 16 | The service logs messages when it starts and stops, and every five seconds 17 | while running. You can see the messages using the Windows Event Viewer 18 | (suggestion: filter for messages from the source "Python Example"). The 19 | service does not do anything else. 20 | 21 | This module depends on the `pywin32` Python package, and is modeled 22 | primarily after the demonstration modules `pipeTestService` and 23 | `serviceEvents` distributed with that package. Additional information 24 | concerning Python implementations of Windows services was gleaned from 25 | various blog and Stack Overflow postings. Information about Windows 26 | services themselves is available from [MSDN](http://msdn.microsoft.com). 27 | 28 | This module can be used either by invoking it as a script, for example: 29 | 30 | python example_service.py start 31 | 32 | or by building a [PyInstaller](http://http://www.pyinstaller.org) 33 | executable from it and then invoking the executable, for example: 34 | 35 | example_service.exe start 36 | 37 | However, invoking the module as a script appears to require that the 38 | path of the directory containing the Python interpreter that should be 39 | used for the service be on the Windows system path, i.e. included in the 40 | value of the system `Path` environment variable. If it is not, then the 41 | start command will fail with a message like: 42 | 43 | Error starting service: The service did not respond to the 44 | start or control request in a timely fashion. 45 | 46 | Using the module as a PyInstaller executable has no such drawback, 47 | since the executable includes a Python interpreter and all needed 48 | packages, so we recommend that approach. 49 | 50 | A typical sequence of commands to test the service of this module is: 51 | 52 | example_service.exe install 53 | example_service.exe start 54 | example_service.exe stop 55 | example_service.exe remove 56 | 57 | For more complete usage information, invoke the executable with the 58 | single argument "help". 59 | 60 | I chose to use PyInstaller rather than py2exe to create an executable 61 | version of this module since as of this writing (January 2017) py2exe 62 | does not yet support Python 3.5. PyInstaller is also cross-platform 63 | while py2exe is not. (That doesn't matter for Windows services, of 64 | course, but I would like something that I also can use with other, 65 | cross-platform Python code.) 66 | """ 67 | 68 | 69 | from logging import Formatter, Handler 70 | import logging 71 | import sys 72 | import os 73 | import subprocess 74 | 75 | import servicemanager 76 | import win32event 77 | import win32service 78 | import win32serviceutil 79 | 80 | 81 | def _main(): 82 | _configure_logging() 83 | 84 | if len(sys.argv) == 1 and \ 85 | sys.argv[0].endswith('.exe') and \ 86 | not sys.argv[0].endswith(r'win32\PythonService.exe'): 87 | # invoked as non-pywin32-PythonService.exe executable without 88 | # arguments 89 | 90 | # We assume here that we were invoked by the Windows Service 91 | # Control Manager (SCM) as a PyInstaller executable in order to 92 | # start our service. 93 | 94 | # Initialize the service manager and start our service. 95 | servicemanager.Initialize() 96 | servicemanager.PrepareToHostSingle(ExampleService) 97 | servicemanager.StartServiceCtrlDispatcher() 98 | else: 99 | # invoked with arguments, or without arguments as a regular 100 | # Python script 101 | 102 | # We support a "help" command that isn't supported by 103 | # `win32serviceutil.HandleCommandLine` so there's a way for 104 | # users who run this script from a PyInstaller executable to see 105 | # help. `win32serviceutil.HandleCommandLine` shows help when 106 | # invoked with no arguments, but without the following that would 107 | # never happen when this script is run from a PyInstaller 108 | # executable since for that case no-argument invocation is handled 109 | # by the `if` block above. 110 | if len(sys.argv) == 2 and sys.argv[1] == 'help': 111 | sys.argv = sys.argv[:1] 112 | 113 | win32serviceutil.HandleCommandLine(ExampleService) 114 | 115 | def _configure_logging(): 116 | formatter = Formatter('%(message)s') 117 | 118 | handler = _Handler() 119 | handler.setFormatter(formatter) 120 | 121 | logger = logging.getLogger() 122 | logger.addHandler(handler) 123 | logger.setLevel(logging.INFO) 124 | 125 | class _Handler(Handler): 126 | def emit(self, record): 127 | servicemanager.LogInfoMsg(record.getMessage()) 128 | 129 | class ExampleService(win32serviceutil.ServiceFramework): 130 | _svc_name_ = 'PythonExample' 131 | _svc_display_name_ = 'Python Example' 132 | _svc_description_ = 'Example of a Windows service implemented in Python.' 133 | _app_exe_name_ = "app-console.exe" 134 | 135 | def __init__(self, args): 136 | win32serviceutil.ServiceFramework.__init__(self, args) 137 | self._stop_event = win32event.CreateEvent(None, 0, 0, None) 138 | 139 | def GetAcceptedControls(self): 140 | result = win32serviceutil.ServiceFramework.GetAcceptedControls(self) 141 | result |= win32service.SERVICE_ACCEPT_PRESHUTDOWN 142 | return result 143 | 144 | def SvcDoRun(self): 145 | _log('has started') 146 | 147 | # determine if application is a script file or frozen exe 148 | if getattr(sys, 'frozen', False): 149 | application_path = os.path.dirname(sys.executable) 150 | elif __file__: 151 | application_path = os.path.dirname(__file__) 152 | 153 | exe_name = os.path.join(application_path, self._app_exe_name_) 154 | _log('launchin subprocess for {exe_name}'.format(exe_name=exe_name)) 155 | p = subprocess.Popen([exe_name]) 156 | 157 | _log('is running in subprocess id {process_id}'.format(process_id=p.pid)) 158 | 159 | while True: 160 | result = win32event.WaitForSingleObject(self._stop_event, 5000) 161 | 162 | if result == win32event.WAIT_OBJECT_0: 163 | # stop requested 164 | _log('is stopping') 165 | p.kill() 166 | break 167 | 168 | _log('has stopped') 169 | 170 | def SvcOtherEx(self, control, event_type, data): 171 | # See the MSDN documentation for "HandlerEx callback" for a list 172 | # of control codes that a service can respond to. 173 | # 174 | # We respond to `SERVICE_CONTROL_PRESHUTDOWN` instead of 175 | # `SERVICE_CONTROL_SHUTDOWN` since it seems that we can't log 176 | # info messages when handling the latter. 177 | if control == win32service.SERVICE_CONTROL_PRESHUTDOWN: 178 | _log('received a pre-shutdown notification') 179 | self._stop() 180 | else: 181 | _log('received an event: code={}, type={}, data={}'.format( 182 | control, event_type, data)) 183 | 184 | 185 | def _stop(self): 186 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 187 | win32event.SetEvent(self._stop_event) 188 | 189 | def SvcStop(self): 190 | self._stop() 191 | 192 | def _log(fragment): 193 | message = 'The {} service {}.'.format(ExampleService._svc_name_, fragment) 194 | logging.info(message) 195 | 196 | if __name__ == '__main__': 197 | _main() --------------------------------------------------------------------------------