Использование PythonService.exe для размещения службы python при использовании virtualenv

У меня есть среда Windows 7, где мне нужно разработать Python Windows Service с использованием Python 3.4. Я использую модуль win32service pywin32 для настройки сервиса, и большинство хуков работают нормально.

Проблема в том, когда я пытаюсь запустить службу из исходного кода (используя 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 для использования моего virtualenv при запуске service.py?

7 ответов

Решение

Похоже, это используется для правильной работы с virtualenv модуль до виртуальных сред были добавлены в Python 3.3. Есть неподтвержденная информация (см. Этот ответ: /questions/39223258/kak-rabotaet-virtualenv/39223278#39223278), что Python site.py используется для поиска вверх от исполняемого файла, пока не найдет каталог, который будет удовлетворять импортам. Затем он использовал бы это для sys.prefix и этого было достаточно для PythonService.exe, чтобы найти virtualenv и использовать его.

Если бы это было поведение, кажется, что site.py больше не делает это с введением venv модуль. Вместо этого он выглядит на один уровень выше pyvenv.cfg файл и настраивает для виртуальной среды только в этом случае. Это, конечно, не работает для PythonService.exe, который скрыт в модуле pywin32 в пакетах сайта.

Чтобы обойти это, я адаптировал activate_this.py код, который поставляется с оригиналом virtualenv модуль (см. этот ответ: /questions/32672440/kak-ya-mogu-aktivirovat-pyvenv-vitrualenv-iz-pitona-activthispy-byil-udalen/32672456#32672456). Он используется для начальной загрузки интерпретатора, встроенного в исполняемый файл (как в случае с 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

Еще один фактор в моих проблемах - это новое колесо pypi для pywin32, которое предоставляют Twisted люди, что облегчает установку с помощью pip. PythonService.exe в этом пакете работал странно (не смог найти DLL-файл pywin32 при вызове) по сравнению с тем, который вы получаете при установке официального пакета win32 exe в виртуальную среду, используя easy_install.

Большое спасибо за размещение этого вопроса и решения. Я выбрал немного другой подход, который также может быть полезен. Довольно сложно найти рабочие советы для служб Python, не говоря уже о том, чтобы делать это с помощью virtualenv. Тем не мение...

меры

Для этого используется Windows 7 x64, Python 3.5.1 x64, pywin32-220 (или pypiwin32-219).

  • Откройте командную строку администратора.
  • Создать виртуал. C:\Python35\python -m venv myvenv
  • Активируйте virtualenv. call myvenv\scripts\activate.bat
  • Установите pywin32, либо:
  • Запустите скрипт после установки python myvenv\Scripts\pywin32_postinstall.py -install,
    • Этот скрипт регистрирует библиотеки DLL в системе и копирует их в C:\Windows\System32, Библиотеки названы pythoncom35.dll а также pywintypes35.dll, Таким образом, виртуальные среды на одной и той же машине с одним и тем же основным выпуском Python разделяют их... это небольшой компромисс:)
  • копия myvenv\Lib\site-packages\win32\pythonservice.exe в myvenv\Scripts\pythonservice.exe
    • В классе обслуживания (какими бы ни были подклассы win32serviceutil.ServiceFramework), установите свойство класса _exe_path_ чтобы указать на этот перемещенный exe. Это станет службой binPath. Например: _exe_path_ = os.path.join(*[os.environ['VIRTUAL_ENV'], 'Scripts', 'pythonservice.exe']),

обсуждение

Я думаю, почему это работает, потому что Python смотрит вверх, чтобы выяснить, где находятся папки Libs, и на основе этого устанавливает пути импорта пакетов, аналогично принятому ответу. Когда pythonservice.exe находится в исходном расположении, это, кажется, не работает гладко.

Это также решает проблемы с связыванием DLL (обнаруживается с помощью зависящего от.exe файла http://www.dependencywalker.com/). Без разбора бизнеса DLL невозможно будет импортировать файлы *.pyd из venv\Lib\site-packages\win32 как модули в ваших скриптах. Например, необходимо разрешить import servicemanager; как servicemanager.pyd отсутствует в пакете в виде файла.py и обладает некоторыми замечательными возможностями журнала событий Windows.

Одна из проблем, возникших у меня с принятым ответом, заключается в том, что я не мог понять, как заставить его точно подобрать пути package.egg-link, которые создаются при использовании setup.py develop, Эти файлы.egg-link включают путь к пакету, если он не находится в virtualenv под myvenv\Lib\site-packages,

Если все прошло гладко, должна быть возможность установить, запустить и протестировать пример службы win32 (из командной строки Admin в активированном 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 в совершенно отдельной среде, чем та, которую вы можете запустить. python myservice.py debug, Так например os.environ['VIRTUAL_ENV'] будет пустым при запуске службы. Это может быть обработано:

  • Установка переменных среды внутри скрипта, например
    • Найти текущий путь, начиная с sys.executable, как описано в принятом ответе.
    • Используйте этот путь, чтобы найти файл конфигурации.
    • Прочитайте файл конфигурации и поместите их в среду с os.environ,
  • Добавьте ключи реестра в службу с переменными среды.

Я прочитал все ответы, но никакое решение не может решить мою проблему.

После тщательного изучения кода Дэвида К. Хесса я внес некоторые изменения, и в конце концов он работает.

Но моей репутации недостаточно, поэтому я просто публикую код здесь.

# 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:
# https://stackru.com/questions/34696815/using-pythonservice-exe-to-host-python-service-while-using-virtualenv
# 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 для работы, указывая правильный интерпретатор с установкой службы.

Для тех, кто читает в 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):
    [...]

Недавно я столкнулся с этой проблемой во встраиваемом интерпретаторе и придумал эти инструкции, которые работают для Python 3.11 как во встраиваемом интерпретаторе, так и в обычной виртуальной среде.

Это решение отличается от самого популярного на данный момент ответа тем, что оно не использует какие-либо общие библиотеки DLL.C:\windows\system32.

Наконец, предоставляется скрипт Python, демонстрирующий необходимую настройку среды.

Виртуальная среда

      PS D:\dev\python_winsvc> C:\Python\Python311\python.exe -m venv venv_311
PS D:\dev\python_winsvc> . .\venv_311\Scripts\activate

(venv_311) PS D:\dev\python_winsvc> pip install pywin32
Collecting pywin32
  Using cached pywin32-306-cp311-cp311-win_amd64.whl (9.2 MB)
Installing collected packages: pywin32
Successfully installed pywin32-306

(venv_311) PS D:\dev\python_winsvc> deactivate

Сделайте так:

      python.exe              # already there
python3.dll             # copy/link from venv source interpreter
python311.dll           # copy/link from venv source interpreter

pythoncom311.dll        # copy/link from .\venv_311\Lib\site-packages\pywin32_system32
pywintypes311.dll       # copy/link from .\venv_311\Lib\site-packages\pywin32_system32

pythonservice.exe       # copy/link from .\venv_311\Lib\site-packages\win32
servicemanager.pyd      # copy/link from .\venv_311\Lib\site-packages\win32

...

Выполните установку службы без активации venv . В противном случае в качестве исполняемого файла службы используется неправильный файл pythonservice.exe, и он должен быть тот, который находится в.\venv_311\Scriptsпоскольку он должен находиться в той же папке, что и требуемая DLL.

      PS D:\dev\python_winsvc> .\venv_311\Scripts\python .\winsvc.py install
Installing service python-winsvc
Service installed

PS D:\dev\python_winsvc> .\venv_311\Scripts\python .\winsvc.py start
Starting service python-winsvc

Встроенный переводчик

Обратите внимание, что мы готовим пакеты pip для встраиваемого интерпретатора, используя регулярно устанавливаемый пакет, поскольку во встраиваемом интерпретаторе отсутствует модуль pip.

      PS D:\dev\python_winsvc> C:\Python\Python311\python.exe -m pip install --target embed_311\lib\site-packages pywin32
Collecting pywin32
  Using cached pywin32-306-cp311-cp311-win_amd64.whl (9.2 MB)
Installing collected packages: pywin32
Successfully installed pywin32-306

Делать.\embed_311выглядеть так:

      python.exe              # already there
python3.dll             # already there
python311.dll           # already there

pythoncom311.dll        # copy/link from .\embed_311\Lib\site-packages\pywin32_system32
pywintypes311.dll       # copy/link from .\embed_311\Lib\site-packages\pywin32_system32

pythonservice.exe       # copy/link from .\embed_311\Lib\site-packages\win32
servicemanager.pyd      # copy/link from .\embed_311\Lib\site-packages\win32

...
      PS D:\dev\python_winsvc> .\embed_311\python.exe .\winsvc.py install
Installing service python-winsvc
Service installed

PS D:\dev\python_winsvc> .\embed_311\python.exe .\winsvc.py start
Starting service python-winsvc

Пример услуги

      # winsvc.py

import sys
import pathlib
PYTHON_PATH = pathlib.Path(sys.executable).parent

import site
site.addsitedir(PYTHON_PATH.joinpath("lib/site-packages"))  # Only required when using the embedded interpreter

from typing import *

import logging, logging.handlers
import threading
import time

import win32event
import win32evtlogutil
import win32service
import win32serviceutil
import servicemanager

def configure_logger(filename: str) -> logging.Logger:
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)

    formatter = logging.Formatter("%(asctime)s %(levelname)5.5s: %(message)s")
    handlers = [
        logging.handlers.RotatingFileHandler(pathlib.Path(__file__).parent.joinpath(filename), maxBytes=1024*1024, backupCount=0), 
        logging.StreamHandler()
    ]
    for handler in handlers:
        handler.setFormatter(formatter)
        logger.addHandler(handler)
    return logger

logger = configure_logger("winsvc.log")


class ApplicationThread(threading.Thread):
    
    def __init__(self) -> None:
        super().__init__()
        self._exit = False

    def stop(self) -> None:
        self._exit = True

    def run(self) -> None:
        logger.debug("service is running")
        while not self._exit:
            time.sleep(1)


class Win32ServiceWrapper(win32serviceutil.ServiceFramework):
    _exe_name_ = str(PYTHON_PATH.joinpath("pythonservice.exe"))
    _svc_name_ = "python-winsvc"
    _svc_display_name_ = "Python WinSvc"

    def __init__(self, args: Iterable[str]) -> None:
        super().__init__(args)
        self._stop_event = win32event.CreateEvent(None, 0, 0, None)
        self._thread = ApplicationThread()

    def SvcStop(self) -> None:
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self._stop_event)

    def SvcDoRun(self):
        win32evtlogutil.ReportEvent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTED, 0, servicemanager.EVENTLOG_INFORMATION_TYPE, (self._svc_name_, ''))
        self._thread.start()
        win32event.WaitForSingleObject(self._stop_event, win32event.INFINITE)
        self._thread.stop()
        self._thread.join()
        win32evtlogutil.ReportEvent(self._svc_display_name_, servicemanager.PYS_SERVICE_STOPPED, 0, servicemanager.EVENTLOG_INFORMATION_TYPE, (self._svc_name_, ''))


if __name__ == "__main__":
    win32serviceutil.HandleCommandLine(Win32ServiceWrapper)

Здесь важно помнить (для тех, кто плохо знаком с сервисами), что при запуске сценария через службы Windows используется PythonService.exe, а при его запуске напрямую используется python.exe.

Команда №1:

      call .\env\Scripts\python3.exe server_windows10_app\\main.py

Результат:

      sys.executable.lower(): c:\u...\env\scripts\python3.exe

Команда №2:

      call .\env\Scripts\python3.exe .\server.py --startup=delayed install
call .\env\Scripts\python3.exe .\server.py start

Результат:

      sys.executable.lower(): c:\...\env\lib\site-packages\win32\pythonservice.exe
Другие вопросы по тегам