I was tasked with building something that would phone home collected data regularly. So I investigated many options but concluded that python + pyinstaller was my best bet. Below are the steps I did and the components I used to craft this solution. I hope this helps someone in the future!

Contents

The Components

The essential items I used to make this work beyond the Python code of the application are the following:

  • Pyinstaller – Package the application up into a windows application.
  • Inno Setup – Installer creation, managed all the installation tasks of deploying and updating the app.
  • pywin32 – Python package for Win32 (pywin32) extensions, which provides access to many Windows APIs from Python. This includes the ability to run as a service.

Code Snippet of Service Application

import socket
import time

import schedule  # https://pypi.python.org/pypi/schedule
import servicemanager
import win32event
import win32service
import win32serviceutil


"""
python.exe XXXX.py install | remove | start | stop | help
XXXX.exe install | remove | start | stop | update | help
"""

.. Add you logic for basic app variables and logging functions here


class WinService(win32serviceutil.ServiceFramework):
    _svc_name_ = "XXXX Service"
    _svc_display_name_ = "XXXX"
    _svc_description_ = "XXXX" \
                        "XXXX" \
                        "XXXX"

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

    # stop command service
    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.stop_event)
        mylogger.info("*** STOPPING SERVICE ***\n")
        self.stop_requested = True
        self.ReportServiceStatus(win32service.SERVICE_STOPPED)

    # start command service
    def SvcDoRun(self):
        try:
            servicemanager.LogMsg(
                servicemanager.EVENTLOG_INFORMATION_TYPE,
                servicemanager.PYS_SERVICE_STARTED,
                (self._svc_name_, '')
            )
            self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
            try:
                # *** STARTING SERVICE ***
                self.ReportServiceStatus(win32service.SERVICE_RUNNING)
                servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED,
                                      (self._svc_name_, ''))
                # run main process
                self.main()
            except Exception as e:
                s = str(e)
                mylogger.error('Exception :' + s)
                self.SvcStop()
        except Exception as e:
            pass

    # main process
    def main(self):
        #... STARTING SCHEDULE PROCESS ...

        # Service Start Logic goes here

        schedule.every(1).minutes.do(YYYY) # Call your Service function you imported, this function should check if actions are needed
        count = 0
        while not self.stop_requested:
            # execute task on schedule
            schedule.run_pending()
            if debug:
                count += 1
                print(f"Sleeping... {count} seconds")
            time.sleep(.5)
            if count == 60 and debug:
                count = 0
        return


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

Code Snippet of Service Installer

REM Script to install/update XXXX
echo off
cls

sc query "XXXX Service" > nul
IF ERRORLEVEL 1060 (
    echo "Service is not installed"
    XXXX.exe --startup delayed install
    sc failure "XXXX Service"  actions= restart/180000/restart/180000/restart/180000 reset= 86400
    XXXX.exe start
) else (
    XXXX.exe stop
    XXXX.exe --startup delayed update
    sc failure "XXXX Service"  actions= restart/180000/restart/180000/restart/180000 reset= 86400
    XXXX.exe start
)

Python base requirements

pyinstaller
pyinstaller-hooks-contrib
pywin32
pywin32-ctypes
schedule