Creating a one-file Windows service in Python with pywin32 and PyInstaller

Written by fredrik

6 december, 2020

dev

I recently set up a brand new Windows service implemented in Python, and although the experience was definitely easier than when I did it last (2006?) there were still a few snags I hit on the way.

First, most (all?) of the service examples I found seem to have been copy-pasted from the same source for ten years, and have some strange incantations that does not seem to (no longer) do anything useful, and although I may of course be wrong about that I don’t like including code I don’t know why it’s there in any project.

Secondly, the way to install self-contained Python programs on Windows has changed, and for the first time I used PyInstaller – which to be fair was a pretty smooth experience for the simple case, even though handling subprocesses and long-running processes took some effort.

Service definition

Using the PyWin32 package can be a bit daunting if you’re not used to Windows programming, but the helpers to create a service are easy enough to use once you find a working example – however, a lot of the code snippets found online and on SO has some extra magic that is not needed, and I wanted a minimal but well-behaved Windows service implementation to start from.

There are two main parts to the service implementation – first there is the command-line helpers to allow you to install the service, and to allow Windows to manage it:

if len(sys.argv) == 1:
    servicemanager.Initialize()
    servicemanager.PrepareToHostSingle(MyServiceFramework)
    servicemanager.StartServiceCtrlDispatcher()
else:
    win32serviceutil.HandleCommandLine(MyServiceFramework)

This is pretty simple – if there is a single argument (the script/binary itself), then start the service control dispatcher to allow Windows to manage the service. If there are any other arguments, assume that it is arguments to manager the service, e.g. install, or start. In both cases, we send our ServiceFramework subclass as a parameter. For a minimal implementation of the ServiceFramework, you need to handle SvcStart and SvcStop:

class MyServiceFramework(win32serviceutil.ServiceFramework):

    _svc_name_ = 'MyService'
    _svc_display_name_ = 'My Service display name'

    def SvcStop(self):
        """Stop the service"""
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        self.service_impl.stop()
        self.ReportServiceStatus(win32service.SERVICE_STOPPED)

    def SvcDoRun(self):
      """Start the service; does not return until stopped"""
        self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
        self.service_impl = MyService()
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        # Run the service
        self.service_impl.run()

In this example MyService is just a class to handle the actual work (sleeping…), in your own implementation it could be anything. I like keeping it entirely separate from the ServiceFramework implementation as it can quickly get complex.

Stand-alone Python executable with PyInstaller

Creating a Windows executable using PyInstaller is reasonably straightforward:

pyinstaller.exe myservice.py

however, this will A) create a folder with the executable and supporting files, and B) not actually work!

The reason it won’t work is that PyInstaller, while very clever, can’t find all imports to include, and in this case will miss including win32timezone in the package. To fix this issue we can tell PyInstaller to include it:

pyinstaller.exe --hidden-import win32timezone myservice.py 

Testing is important here – the build will succeed and then the program will fail in run time, so for complex application it becomes a case of trial and error – building, running to find any missing imports, then re-building after adding new --hidden-imports.

With this, the service works and we could install it with myservice.exe install, but I find I’d rather have a single file, which can be accomplished using the --onefile flag:

pyinstaller.exe --onefile --hidden-import win32timezone myservice.py

This creates a single executable, easy to distribute, and it will look like it works. However, it will unpack files in the Windows temp folder, which normally gets periodically wiped – this is likely to break any non-trivial service running on Windows for a long time. The fix for this issue is to unpack it somewhere where Windows will leave it alone, for example the current folder whatever that may be:

pyinstaller.exe --runtime-tmpdir=. --onefile --hidden-import win32timezone myservice.py

This should work – but due to issue 4579 on PyInstaller (fix incoming here), can break unexpectedly depending on what you set the runtime-tmpdir to – my solution has been to install PyInstaller from the PR until it is merged and released.

Full code listing

And there you have it! A fully working, single-executable Windows service. This code has been tested using a Python 3.7 virtualenv on Windows 10, and the resulting services has been running successfully on a wide variety of mostly older Windows versions (without Python installed).

"""
# Prerequisites:
pip3 install pywin32 pyinstaller

# Build:
pyinstaller.exe --onefile --runtime-tmpdir=. --hidden-import win32timezone myservice.py

# With Administrator privilges
# Install:
dist\myservice.exe install

# Start:
dist\myservice.exe start

# Install with autostart:
dist\myservice.exe --startup delayed install

# Debug:
dist\myservice.exe debug

# Stop:
dist\myservice.exe stop

# Uninstall:
dist\myservice.exe remove


"""

import time

import win32serviceutil  # ServiceFramework and commandline helper
import win32service  # Events
import servicemanager  # Simple setup and logging

class MyService:
    """Silly little application stub"""
    def stop(self):
        """Stop the service"""
        self.running = False

    def run(self):
        """Main service loop. This is where work is done!"""
        self.running = True
        while self.running:
            time.sleep(10)  # Important work
            servicemanager.LogInfoMsg("Service running...")


class MyServiceFramework(win32serviceutil.ServiceFramework):

    _svc_name_ = 'MyService'
    _svc_display_name_ = 'My Service display name'

    def SvcStop(self):
        """Stop the service"""
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        self.service_impl.stop()
        self.ReportServiceStatus(win32service.SERVICE_STOPPED)

    def SvcDoRun(self):
      """Start the service; does not return until stopped"""
        self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
        self.service_impl = MyService()
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        # Run the service
        self.service_impl.run()


def init():
    if len(sys.argv) == 1:
        servicemanager.Initialize()
        servicemanager.PrepareToHostSingle(MyServiceFramework)
        servicemanager.StartServiceCtrlDispatcher()
    else:
        win32serviceutil.HandleCommandLine(MyServiceFramework)


if __name__ == '__main__':
    init()

You May Also Like…

What I wish recruiters would do

I spend a lot of time dealing with recruiters - responding to queries, and accepting or rejecting endless amount of...

0 kommentarer