在使用 virtualenv 时使用 PythonService.exe 托管 python 服务

Using PythonService.exe to host python service while using virtualenv

我有一个 Windows 7 环境,我需要在其中使用 Python 3.4 开发 Python Windows 服务。我正在使用 pywin32 的 win32service 模块来设置服务,大多数挂钩似乎工作正常。

问题是当我尝试从源代码 运行 服务时(使用 python service.py install 后跟 python service.py start)。这使用 PythonService.exe 来托管 service.py - 但我使用的是 venv 虚拟环境并且脚本找不到它的模块(错误消息发现 python service.py debug)。

Pywin32 安装在 virtualenv 中,在查看 PythonService.exe 的源代码时,它动态链接 Python34.dll,导入我的 service.py 并调用它。

如何让 PythonService.exe 在 运行 连接我的 service.py 时使用我的 virtualenv?

在将虚拟环境添加到 Python 3.3 之前,这似乎与 virtualenv 模块一起正常工作。有轶事证据(请参阅此答案:),Python 的 site.py 过去常常从可执行文件向上查找,直到找到满足导入的目录。然后它将用于 sys.prefix,这足以让 PythonService.exe 找到它所在的 virtualenv 并使用它。

如果那是行为,那么随着 venv 模块的引入,site.py 似乎不再这样做。相反,它会向上一级查找 pyvenv.cfg 文件,并仅在这种情况下为虚拟环境进行配置。这当然不适用于 PythonService.exe ,它隐藏在 site-packages.

下的 pywin32 模块中

为了解决这个问题,我改编了原始 virtualenv 模块附带的 activate_this.py 代码(请参阅此答案:)。它用于 bootstrap 嵌入在可执行文件中的解释器(PythonService.exe 就是这种情况)使用 virtualenv。不幸的是,venv 不包括这个。

以下是对我有用的方法。请注意,这假设虚拟环境被命名为 my-venv 并且位于源代码位置的上一层。

import os
import sys

if sys.executable.endswith("PythonService.exe"):

    # Change current working directory from PythonService.exe location to something better.
    service_directory = os.path.dirname(__file__)
    source_directory = os.path.abspath(os.path.join(service_directory, ".."))
    os.chdir(source_directory)
    sys.path.append(".")

    # Adapted from virtualenv's activate_this.py
    # Manually activate a virtual environment inside an already initialized interpreter.
    old_os_path = os.environ['PATH']
    venv_base = os.path.abspath(os.path.join(source_directory, "..", "my-venv"))
    os.environ['PATH'] = os.path.join(venv_base, "Scripts") + os.pathsep + old_os_path
    site_packages = os.path.join(venv_base, 'Lib', 'site-packages')
    prev_sys_path = list(sys.path)
    import site
    site.addsitedir(site_packages)
    sys.real_prefix = sys.prefix
    sys.prefix = venv_base

    new_sys_path = []
    for item in list(sys.path):
        if item not in prev_sys_path:
            new_sys_path.append(item)
            sys.path.remove(item)
    sys.path[:0] = new_sys_path

我遇到麻烦的另一个因素 - Twisted 人员提供了一个用于 pywin32 的新 pypi wheel,可以更轻松地使用 pip 安装。与使用 easy_install.

非常感谢您发布这个问题和解决方案。我采取了一种略有不同的方法,这可能也很有用。很难找到 Python 服务的工作技巧,更不用说使用 virtualenv 了。无论如何...

步骤

这是使用 Windows 7 x64,Python 3.5.1 x64,pywin32-220(或 pypiwin32-219)。

  • 打开管理员命令提示符。
  • 创建一个虚拟环境。 C:\Python35\python -m venv myvenv
  • 激活虚拟环境。 call myvenv\scripts\activate.bat
  • 安装 pywin32,或者:
  • 运行 post-install 脚本 python myvenv\Scripts\pywin32_postinstall.py -install
    • 此脚本在系统中注册 DLL,并将它们复制到 C:\Windows\System32。 DLL 的名称为 pythoncom35.dllpywintypes35.dll。因此,同一台机器上同一主要 Python 点版本的虚拟环境将共享这些...这是一个小的权衡:)
  • 复制myvenv\Lib\site-packages\win32\pythonservice.exemyvenv\Scripts\pythonservice.exe
    • 在服务 class 上(无论子classes win32serviceutil.ServiceFramework),设置 class 属性 _exe_path_ 指向这个搬迁的exe。这将成为服务 binPath。例如:_exe_path_ = os.path.join(*[os.environ['VIRTUAL_ENV'], 'Scripts', 'pythonservice.exe']).

讨论

我认为这有效的原因是 Python 向上查找 Libs 文件夹的位置,并基于此设置包导入路径,类似于已接受的答案。当 pythonservice.exe 位于原始位置时,这似乎并不顺利。

它还解决了 DLL 链接问题(可通过 http://www.dependencywalker.com/ 中的 depends.exe 发现)。如果不解决 DLL 业务,将无法从 venv\Lib\site-packages\win32 的 *.pyd 文件作为脚本中的模块导入。例如需要允许 import servicemanager;因为 servicemanager.pyd 不在包中作为 .py 文件,并且具有一些很酷的 Windows 事件日志功能。

我在接受答案时遇到的一个问题是我无法弄清楚如何让它准确地提取包裹。egg-link 使用 setup.py develop 时创建的路径.这些 .egg-link 文件包含包的路径,当它不在 myvenv\Lib\site-packages.

下的 virtualenv 中时

如果一切顺利,应该可以安装、启动和测试示例 win32 服务(从激活的 virtualenv 中的管理员提示符):

python venv\Lib\site-packages\win32\Demos\service\pipeTestService.py install
python venv\Lib\site-packages\win32\Demos\service\pipeTestService.py start
python venv\Lib\site-packages\win32\Demos\service\pipeTestServiceClient.py

服务环境

所有这一切中的另一个重要说明是,该服务将在与您可能 运行 python myservice.py debug 完全不同的环境中执行 python 代码。因此,例如 os.environ['VIRTUAL_ENV'] 将在 运行 服务时为空。这可以通过以下任一方式处理:

  • 从脚本内部设置环境变量,例如
    • 从 sys.executable 开始查找当前路径,如接受的答案中所述。
    • 使用该路径查找配置文件。
    • 读取配置文件并将它们放在os.environ的环境中。
  • 使用环境变量向服务添加注册表项。
    • 请参阅 Accessing Environment Variables from Windows Services 使用 regedit.exe
    • 手动执行此操作
    • 请参阅 REG ADD a REG_MULTI_SZ Multi-Line Registry Value 从命令行执行此操作。

对于任何在 2018 年阅读的人来说,我对上述任何一个解决方案(Win10,Python 3.6)都没有任何运气 - 所以这就是我为让它工作所做的。工作目录在启动时位于 site-packages/win32 中,因此您需要在尝试导入任何项目代码之前更改工作目录并修复 sys.path。这假定 venv 位于您的项目目录中,否则您可能只需要对一些路径进行硬编码:

import sys
import os
if sys.executable.lower().endswith("pythonservice.exe"):
    for i in range(4): # goes up 4 directories to project folder
        os.chdir("..")        
    # insert site-packages 2nd in path (behind project folder)
    sys.path.insert(1, os.path.join("venv",'Lib','site-packages'))

[REST OF IMPORTS]
class TestService(win32serviceutil.ServiceFramework):
    [...]

我阅读了所有答案,但没有解决方案可以解决我的问题。

仔细研究的代码后,我做了一些修改,终于可以了。

但是我的声望不够,所以我只是post这里的代码。

# 1. Custom your Project's name and Virtual Environment folder's name
# 2. Import this before all third part models
# 3. If you still failed, check the link below:
# 
# 2019-05-29 by oraant, modified from David K. Hess's answer.

import os, sys, site

project_name = "PythonService"  # Change this for your own project !!!!!!!!!!!!!!
venv_folder_name = "venv"  # Change this for your own venv path !!!!!!!!!!!!!!

if sys.executable.lower().endswith("pythonservice.exe"):

    # Get root path for the project
    service_directory = os.path.abspath(os.path.dirname(__file__))
    project_directory = service_directory[:service_directory.find(project_name)+len(project_name)]

    # Get venv path for the project
    def file_path(x): return os.path.join(project_directory, x)
    venv_base = file_path(venv_folder_name)
    venv_scripts = os.path.join(venv_base, "Scripts")
    venv_packages = os.path.join(venv_base, 'Lib', 'site-packages')

    # Change current working directory from PythonService.exe location to something better.
    os.chdir(project_directory)
    sys.path.append(".")
    prev_sys_path = list(sys.path)

    # Manually activate a virtual environment inside an already initialized interpreter.
    os.environ['PATH'] = venv_scripts + os.pathsep + os.environ['PATH']

    site.addsitedir(venv_packages)
    sys.real_prefix = sys.prefix
    sys.prefix = venv_base

    # Move some sys path in front of others
    new_sys_path = []
    for item in list(sys.path):
        if item not in prev_sys_path:
            new_sys_path.append(item)
            sys.path.remove(item)
    sys.path[:0] = new_sys_path

如何使用?很简单,只需将其粘贴到一个新的 python 文件中,然后在任何第三方模型之前导入它,如下所示:

import service_in_venv  # import at top
import win32serviceutil
import win32service
import win32event
import servicemanager
import time
import sys, os
........

现在你应该解决你的问题了。

不使用“pythonservice.exe”,直接注册python.exe服务:

import win32serviceutil
import win32service
import servicemanager
import sys
import os
import os.path
import multiprocessing

# 

def main():
    import time
    time.sleep(600)  

class ProcessService(win32serviceutil.ServiceFramework):
    _svc_name_ = "SleepService"
    _svc_display_name_ = "Sleep Service"
    _svc_description_ = "Sleeps for 600"
    _exe_name_ = sys.executable # python.exe from venv
    _exe_args_ = '-u -E "' + os.path.abspath(__file__) + '"'

    proc = None

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        if self.proc:
            self.proc.terminate()

    def SvcRun(self):
        self.proc = multiprocessing.Process(target=main)
        self.proc.start()        
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        self.SvcDoRun()
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)

    def SvcDoRun(self):
        self.proc.join()

def start():
    if len(sys.argv)==1:
        import win32traceutil
        servicemanager.Initialize()
        servicemanager.PrepareToHostSingle(ProcessService)
        servicemanager.StartServiceCtrlDispatcher()
    elif '--fg' in sys.argv:
        main()
    else:
        win32serviceutil.HandleCommandLine(ProcessService)

if __name__ == '__main__':
    try:
        start()
    except (SystemExit, KeyboardInterrupt):
        raise
    except:
        import traceback
        traceback.print_exc()

它使 python 3.5+ virtualenv 支持通过服务安装指向正确的 iterpreter 来工作。