如何在Windows中将Python脚本作为服务运行?

340

我正在为一组程序勾勒架构,这些程序共享存储在数据库中的各种相互关联的对象。我希望其中一个程序作为服务提供对这些对象进行操作的更高级别接口,而其他程序通过该服务访问这些对象。

目前,我计划使用Python和Django框架实现该服务。我相当确定如何在Linux中将Python程序设为守护进程。然而,系统应支持Windows是可选规格要求,我在Windows编程方面经验很少,也没有任何关于Windows服务的经验。

能否将Python程序作为Windows服务运行(即自动运行而无需用户登录)? 我不一定非要实现这个部分,但需要大致了解如何完成,以便决定是否按照这些线路进行设计。

编辑:感谢迄今为止所有答案,它们相当全面。我还想知道一件事:Windows如何知道我的服务?我能用原生Windows工具管理它吗? /etc/init.d中start/stop脚本的等价物是什么?

14个回答

2

这篇答案是从StackOverflow上多个来源抄袭的 - 大部分在上面,但我忘记了其他的 - 抱歉。它很简单,脚本可以"原样"运行。对于你要测试的发布版本,你需要测试你的脚本,然后将其复制到服务器并停止/启动相关服务。它适用于所有脚本语言(Python、Perl、node.js)以及批处理脚本,如GitBash、PowerShell,甚至旧的DOS批处理脚本。 pyGlue是连接Windows服务和你的脚本之间的粘合剂。

'''
A script to create a Windows Service, which, when started, will run an executable with the specified parameters.
Optionally, you can also specify a startup directory

To use this script you MUST define (in class Service)
1. A name for your service (short - preferably no spaces)
2. A display name for your service (the name visibile in Windows Services)
3. A description for your service (long details visible when you inspect the service in Windows Services)
4. The full path of the executable (usually C:/Python38/python.exe or C:WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe
5. The script which Python or PowerShell will run(or specify None if your executable is standalone - in which case you don't need pyGlue)
6. The startup directory (or specify None)
7. Any parameters for your script (or for your executable if you have no script)

NOTE: This does not make a portable script.
The associated '_svc_name.exe' in the dist folder will only work if the executable,
(and any optional startup directory) actually exist in those locations on the target system

Usage: 'pyGlue.exe [options] install|update|remove|start [...]|stop|restart [...]|debug [...]'
Options for 'install' and 'update' commands only:
        --username domain\\username : The Username the service is to run under
        --password password : The password for the username
        --startup [manual|auto|disabled|delayed] : How the service starts, default = manual
        --interactive : Allow the service to interact with the desktop.
        --perfmonini file: .ini file to use for registering performance monitor data
        --perfmondll file: .dll file to use when querying the service for performance data, default = perfmondata.dll
Options for 'start' and 'stop' commands only:
        --wait seconds: Wait for the service to actually start or stop.
                If you specify --wait with the 'stop' option, the service and all dependent services will be stopped,
                each waiting the specified period.
'''

# Import all the modules that make life easy
import servicemanager
import socket
import sys
import win32event
import win32service
import win32serviceutil
import win32evtlogutil
import os
from logging import Formatter, Handler
import logging
import subprocess


# Define the win32api class
class Service (win32serviceutil.ServiceFramework):
        # The following variable are edited by the build.sh script
        _svc_name_ = "TestService"
        _svc_display_name_ = "Test Service"
        _svc_description_ = "Test Running Python Scripts as a Service"
        service_exe = 'c:/Python27/python.exe'
        service_script = None
        service_params = []
        service_startDir = None

        # Initialize the service
        def __init__(self, args):
                win32serviceutil.ServiceFramework.__init__(self, args)
                self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
                self.configure_logging()
                socket.setdefaulttimeout(60)

        # Configure logging to the WINDOWS Event logs
        def configure_logging(self):
                self.formatter = Formatter('%(message)s')
                self.handler = logHandler()
                self.handler.setFormatter(self.formatter)
                self.logger = logging.getLogger()
                self.logger.addHandler(self.handler)
                self.logger.setLevel(logging.INFO)

        # Stop the service
        def SvcStop(self):
                self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
                win32event.SetEvent(self.hWaitStop)

        # Run the service
        def SvcDoRun(self):
                self.main()

        # This is the service
        def main(self):

                # Log that we are starting
                servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED,
                                                          (self._svc_name_, ''))

                # Fire off the real process that does the real work
                logging.info('%s - about to call Popen() to run %s %s %s', self._svc_name_, self.service_exe, self.service_script, self.service_params)
                self.process = subprocess.Popen([self.service_exe, self.service_script] + self.service_params, shell=False, cwd=self.service_startDir)
                logging.info('%s - started process %d', self._svc_name_, self.process.pid)

                # Wait until WINDOWS kills us - retrigger the wait for stop every 60 seconds
                rc = None
                while rc != win32event.WAIT_OBJECT_0:
                        rc = win32event.WaitForSingleObject(self.hWaitStop, (1 * 60 * 1000))

                # Shut down the real process and exit
                logging.info('%s - is terminating process %d', self._svc_name_, self.process.pid)
                self.process.terminate()
                logging.info('%s - is exiting', self._svc_name_)


class logHandler(Handler):
        '''
Emit a log record to the WINDOWS Event log
        '''

        def emit(self, record):
                servicemanager.LogInfoMsg(record.getMessage())


# The main code
if __name__ == '__main__':
        '''
Create a Windows Service, which, when started, will run an executable with the specified parameters.
        '''

        # Check that configuration contains valid values just in case this service has accidentally
        # been moved to a server where things are in different places
        if not os.path.isfile(Service.service_exe):
                print('Executable file({!s}) does not exist'.format(Service.service_exe), file=sys.stderr)
                sys.exit(0)
        if not os.access(Service.service_exe, os.X_OK):
                print('Executable file({!s}) is not executable'.format(Service.service_exe), file=sys.stderr)
                sys.exit(0)
        # Check that any optional startup directory exists
        if (Service.service_startDir is not None) and (not os.path.isdir(Service.service_startDir)):
                print('Start up directory({!s}) does not exist'.format(Service.service_startDir), file=sys.stderr)
                sys.exit(0)

        if len(sys.argv) == 1:
                servicemanager.Initialize()
                servicemanager.PrepareToHostSingle(Service)
                servicemanager.StartServiceCtrlDispatcher()
        else:
                # install/update/remove/start/stop/restart or debug the service
                # One of those command line options must be specified
                win32serviceutil.HandleCommandLine(Service)

现在需要进行一些编辑,您不希望所有的服务都叫做“pyGlue”。因此有一个脚本(build.sh)可以插入这些部分并创建一个定制的“pyGlue”以及创建一个“.exe”文件。这个“.exe”文件将被安装为Windows服务。安装完成后,您可以将其设置为自动运行。
#!/bin/sh
# This script build a Windows Service that will install/start/stop/remove a service that runs a script
# That is, executes Python to run a Python script, or PowerShell to run a PowerShell script, etc

if [ $# -lt 6 ]; then
        echo "Usage: build.sh Name Display Description Executable Script StartupDir [Params]..."
        exit 0
fi

name=$1
display=$2
desc=$3
exe=$4
script=$5
startDir=$6
shift; shift; shift; shift; shift; shift
params=
while [ $# -gt 0 ]; do
        if [ "${params}" != "" ]; then
                params="${params}, "
        fi
        params="${params}'$1'"
        shift
done

cat pyGlue.py | sed -e "s/pyGlue/${name}/g" | \
        sed -e "/_svc_name_ =/s?=.*?= '${name}'?" | \
        sed -e "/_svc_display_name_ =/s?=.*?= '${display}'?" | \
        sed -e "/_svc_description_ =/s?=.*?= '${desc}'?" | \
        sed -e "/service_exe =/s?=.*?= '$exe'?" | \
        sed -e "/service_script =/s?=.*?= '$script'?" | \
        sed -e "/service_params =/s?=.*?= [${params}]?" | \
        sed -e "/service_startDir =/s?=.*?= '${startDir}'?" > ${name}.py

cxfreeze ${name}.py --include-modules=win32timezone

安装 - 将 '.exe' 文件和脚本复制到指定文件夹中。以管理员身份运行 '.exe',选择 'install' 选项。以管理员身份打开 Windows 服务,并启动您的服务。升级时,只需复制脚本的新版本并停止/启动服务即可。

现在每个服务器都不同 - Python 的安装不同,文件夹结构也不同。我为每个服务器维护一个文件夹,其中包含 pyGlue.py 和 build.sh 的副本。我还创建了一个名为 'serverBuild.sh' 的脚本,用于重新构建该服务器上的所有服务。

# A script to build all the script based Services on this PC
sh build.sh AutoCode 'AutoCode Medical Documents' 'Autocode Medical Documents to SNOMED_CT and AIHW codes' C:/Python38/python.exe autocode.py C:/Users/russell/Documents/autocoding -S -T

2

使用循环或子线程的完整 pywin32 示例

在断断续续地工作了几天之后,我终于得出了这个答案,使用pywin32使其保持良好的自包含性。

这是一个基于循环和基于线程的解决方案的完整可工作代码。它可以在python 2和3上工作,尽管我只测试过2.7和Win7上的最新版本。循环应该适用于轮询代码,而线程应该适用于更类似服务器的代码。它似乎可以与没有标准优雅关闭方式的Waitress WSGI服务器很好地配合使用。

我还想指出,似乎有很多例子存在,例如 这个 ,它们几乎有用,但实际上是误导性的,因为它们盲目地剪切和粘贴其他示例。我可能是错的。但如果你从不等待事件,为什么要创建事件呢?

话虽如此,我仍然觉得在这里我有些站不稳脚跟,尤其是关于线程版本退出的清晰度,但至少我相信这里没有任何误导性。

要运行,只需将代码复制到文件中并按照说明执行即可。

更新:

使用简单的标志来终止线程。重要的是,“线程完成”被打印出来。
有关如何从不合作的服务器线程中退出的更详细示例,请参见我的关于Waitress WSGI服务器的帖子

# uncomment mainthread() or mainloop() call below
# run without parameters to see HandleCommandLine options
# install service with "install" and remove with "remove"
# run with "debug" to see print statements
# with "start" and "stop" watch for files to appear
# check Windows EventViever for log messages

import socket
import sys
import threading
import time
from random import randint
from os import path

import servicemanager
import win32event
import win32service
import win32serviceutil
# see http://timgolden.me.uk/pywin32-docs/contents.html for details


def dummytask_once(msg='once'):
    fn = path.join(path.dirname(__file__),
                '%s_%s.txt' % (msg, randint(1, 10000)))
    with open(fn, 'w') as fh:
        print(fn)
        fh.write('')


def dummytask_loop():
    global do_run
    while do_run:
        dummytask_once(msg='loop')
        time.sleep(3)


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

    def run(self):
        global do_run
        do_run = True
        print('thread start\n')
        dummytask_loop()
        print('thread done\n')

    def exit(self):
        global do_run
        do_run = False


class SMWinservice(win32serviceutil.ServiceFramework):
    _svc_name_ = 'PyWinSvc'
    _svc_display_name_ = 'Python Windows Service'
    _svc_description_ = 'An example of a windows service in Python'

    @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)  # create generic event
        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)  # raise event

    def SvcDoRun(self):
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
                            servicemanager.PYS_SERVICE_STARTED,
                            (self._svc_name_, ''))
        # UNCOMMENT ONE OF THESE
        # self.mainthread()
        # self.mainloop()

    # Wait for stopEvt indefinitely after starting thread.
    def mainthread(self):
        print('main start')
        self.server = MyThread()
        self.server.start()
        print('wait for win32event')
        win32event.WaitForSingleObject(self.stopEvt, win32event.INFINITE)
        self.server.exit()
        print('wait for thread')
        self.server.join()
        print('main done')

    # Wait for stopEvt event in loop.
    def mainloop(self):
        print('loop start')
        rc = None
        while rc != win32event.WAIT_OBJECT_0:
            dummytask_once()
            rc = win32event.WaitForSingleObject(self.stopEvt, 3000)
        print('loop done')


if __name__ == '__main__':
    SMWinservice.parse_command_line()

这是一个在后台运行而没有控制台的程序,打印命令输出的信息会在哪里? - elsadek
打印输出是为了演示它在CLI上的工作原理。如果使用调试标志作为服务运行,它们也将显示出来。 - Alias_Knagg

1
使用win32serviceutil的被接受答案虽然可行,但较为复杂,使得调试和更改变得更加困难。使用NSSM(the Non-Sucking Service Manager)要容易得多。您可以编写并轻松调试普通的Python程序,然后在不到一分钟的时间内使用NSSM将其安装为服务:

从提升的(管理员)命令提示符中运行nssm.exe install NameOfYourService并填写这些选项:

  • path: (python.exe路径,例如C:\Python27\Python.exe
  • Arguments: (您的Python脚本路径,例如c:\path\to\program.py

顺便说一下,如果您的程序打印出有用的消息,并且您想将其保存在日志文件中,NSSM也可以为您处理这个问题以及更多其他问题。


是的,这是Adriano答案的重复。我投了赞成票并尝试编辑它,但在编辑后,我看到了一个新的答案。 - ndemou
如何提供虚拟环境? - shaik moeed

-2

https://www.chrisumbel.com/article/windows_services_in_python

  1. 跟进 PySvc.py

  2. 更改 dll 文件夹

我知道这是老问题,但我卡在这里很久。对我来说,这个特定的问题是通过复制这个文件 - pywintypes36.dll 解决的。

从 -> Python36\Lib\site-packages\pywin32_system32

到 -> Python36\Lib\site-packages\win32

setx /M PATH "%PATH%;C:\Users\user\AppData\Local\Programs\Python\Python38-32;C:\Users\user\AppData\Local\Programs\Python\Python38-32\Scripts;C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\pywin32_system32;C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\win32
  1. 通过以下方式更改Python文件夹的路径

cd C:\Users\user\AppData\Local\Programs\Python\Python38-32

  1. NET START PySvc
  2. NET STOP PySvc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接