

# 質問内容

後述する環境・設定・ソースにおいて、サービス登録後に開始・成功するにはどうすればよいでしょうか？

# 環境

Windows 10 Pro  
Python 3.9.10 (pyenv+venv)  
pip 23.0.1  
pywin32 305

# 現象

venvでの仮想環境で、以下のサービス登録は成功します。管理ツールｰ>サービスを起動して、一覧にも登録されていることを確認しました。

[code]

    > python app.py install
    Installing service test_pywin32_service
    copying host exe 'C:\...\ENV\lib\site-packages\win32\pythonservice.exe' -> 'C:\...\ENV\pythonservice.exe'
    Service installed
    
[/code]

サービスを実行しようとすると、失敗します。

[code]

    > python app.py start
    Starting service test_pywin32_service
    Error starting service: そのサービスは指定時間内に開始要求または制御要求に応答しませんでした。
    
[/code]

# やってみたこと

## システム環境変数のPathに追加

[code]

    C:\...\ENV\Scripts\  ... python.exeがあります。
    C:\...\ENV\Lib\site-packages\pywin32_system32
    C:\...\ENV\Lib\site-packages\win32
    
[/code]

以下のコマンドでPathに正しく登録されたことを確認しました。

[code]

    echo %Path:;=&echo.%
    
[/code]

この環境変数への適用により、pyenvよりも優先して、venv環境のPythonが実行されることを確認しました。  
具体的には、以下のコマンドにより、インストールされているパッケージリストの違いにより判断しました。

[code]

    python -m pip list 
    
[/code]

# pip インストールパッケージ

[code]

    autopep8==2.0.2
      - pycodestyle [required: >=2.10.0, installed: 2.10.0]
      - tomli [required: Any, installed: 2.0.1]
    flake8==6.0.0
      - mccabe [required: >=0.7.0,<0.8.0, installed: 0.7.0]
      - pycodestyle [required: >=2.10.0,<2.11.0, installed: 2.10.0]
      - pyflakes [required: >=3.0.0,<3.1.0, installed: 3.0.1]
    pip-licenses==4.1.0
      - prettytable [required: >=2.3.0, installed: 3.6.0]
        - wcwidth [required: Any, installed: 0.2.6]
    pip-review==1.3.0
      - packaging [required: Any, installed: 23.0]
      - pip [required: Any, installed: 23.0.1]
    pipdeptree==2.5.2
    pywin32==305
    setuptools==58.1.0
    wheel==0.38.4
    
[/code]

# ソースコード

app.py

[code]

    import win32serviceutil
    import win32service
    import win32event
    import servicemanager
    import logging
    import os
    import pathlib
    import time
    
    log_path = os.getenv("TEMP") + "\\test_pywin32_service\\test-service.log"
    if not os.path.exists(log_path):
        ret_path = os.path.split(log_path)
        pathlib.Path(ret_path[0]).mkdir(
            parents=True,
            exist_ok=True
        )
    
    logging.basicConfig(
        filename=log_path,
        level=logging.DEBUG,
        format="%(asctime)s:LINE[%(lineno)s] %(levelname)s %(message)s"
    )
    
    
    class test_pywin32_service(win32serviceutil.ServiceFramework):
        _svc_name_ = "test_pywin32_service"
        _svc_display_name_ = "TEST Service"
        _svc_description_ = '一定周期でログを出力する'
        _timeout_Milliseconds = 10000
    
        def __init__(self, args):
            win32serviceutil.ServiceFramework.__init__(self, args)
            self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
    
        def SvcStop(self):
            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
            win32event.SetEvent(self.hWaitStop)
    
        def SvcDoRun(self):
            servicemanager.LogMsg(
                servicemanager.EVENTLOG_INFORMATION_TYPE,
                servicemanager.PYS_SERVICE_STARTED,
                (self._svc_name_, '')
            )
    
            while True:
                ret = win32event.WaitForSingleObject(
                    self.hWaitStop,
                    self._timeout_Milliseconds
                )
                if ret == win32event.WAIT_OBJECT_0:
                    break
    
                time.sleep(5)
    
                logging.info(__class__.__name__)
    
    
    if __name__ == '__main__':
        logging.debug('start')
        win32serviceutil.HandleCommandLine(test_pywin32_service)
    
[/code]


CC BY-SA 4.0
2023-03-14T04:11:38.627
94163
2023-03-15T14:57:18.650
2023-03-15T10:13:09.090
3060
32891
question
python pyenv python-venv pywin32
pyenv+venv 環境でpywin32によるサービス開始ができない


ご紹介いただいた以下のサイトを参考にしたところ、サービス起動が成功しました。  
[How to Run Python Script as a Service (Windows &
Linux)](https://oxylabs.io/blog/python-script-service-guide)

最終的にPyInstallerでの提供を考えていたので、とても助かりました。

# ソースコード修正

[code]

    import win32serviceutil
    import win32service
    import win32event
    import servicemanager
    import logging
    import logging.handlers
    import os
    import pathlib
    import sys
    import time
    
    
    def _logger_():
        log_path = os.getenv("TEMP") + "\\test_pywin32_service\\test-service.log"
        if not os.path.exists(log_path):
            ret_path = os.path.split(log_path)
            pathlib.Path(ret_path[0]).mkdir(
                parents=True,
                exist_ok=True
            )
    
        logging_format = '%(asctime)s %(threadName)s %(levelname)s:%(message)s'
        formatter = logging.Formatter(logging_format)
    
        root = logging.getLogger()
    
        handler = logging.handlers.RotatingFileHandler(
            filename=log_path,
            mode="a",
            maxBytes=1*1024*1024,
            backupCount=2,
        )
        handler.setFormatter(formatter)
        root.addHandler(handler)
        root.setLevel('DEBUG')
    
        return root
    
    
    class test_pywin32_service(win32serviceutil.ServiceFramework):
        # サービス名
        _svc_name_ = "test_pywin32_service"
        # 表示名(サービス画面にはこれを表示)
        _svc_display_name_ = "TEST Service"
        # サービスの説明
        _svc_description_ = '一定周期でログを出力する'
        # シグナルまでの待ち用タイムアウト時間(今回は10秒 = 10,000ミリ秒)
        _timeout_Milliseconds = 10000
    
        def __init__(self, args):
            self.logger = _logger_()
            self.logger.info('init')
            win32serviceutil.ServiceFramework.__init__(self, args)
            self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
    
        def SvcStop(self):
            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
            win32event.SetEvent(self.hWaitStop)
    
        def SvcDoRun(self):
            self.logger.info('run')
            servicemanager.LogMsg(
                servicemanager.EVENTLOG_INFORMATION_TYPE,
                servicemanager.PYS_SERVICE_STARTED,
                (self._svc_name_, '')
            )
    
            self.logger.info('start')
    
            while True:
                self.logger.info('alive')
                # イベントのシグナル化を10秒待つ
                ret = win32event.WaitForSingleObject(
                    self.hWaitStop,
                    self._timeout_Milliseconds
                )
                # サービス停止(イベントがシグナル化)ならば、処理を中止
                if ret == win32event.WAIT_OBJECT_0:
                    break
    
                time.sleep(1)
    
            self.logger.info('finish')
    
    
    if __name__ == '__main__':
        if len(sys.argv) == 1:
            servicemanager.Initialize()
            servicemanager.PrepareToHostSingle(test_pywin32_service)
            servicemanager.StartServiceCtrlDispatcher()
        else:
            win32serviceutil.HandleCommandLine(test_pywin32_service)
    
    
[/code]

# サービス実行

PyInstallerでexe化

[code]

    PyInstaller --hiddenimport win32timezone -F .\source\app.py
    
[/code]

サービス登録

[code]

    .\dist\app.exe install
    
[/code]

サービスデバッグ起動

[code]

    .\dist\app.exe debug
    
[/code]

サービス開始

[code]

    .\dist\app.exe start
    
[/code]

サービス停止

[code]

    .\dist\app.exe stop
    
[/code]

サービス削除

[code]

    .\dist\app.exe remove
    
[/code]

# 新たな問題

debug起動では正常にログに出力できますが、start起動ではTempにフォルダ作成からできていないことが分かりました。この問題は、別の質問として投稿します。


CC BY-SA 4.0
2023-03-14T06:22:47.220
94165
2023-03-14T06:22:47.220
32891
94163
answer


# 回答ではありません。

pywin32のサービスアプリの実体を調べたところ、以下に行き着きました。

<https://github.com/mhammond/pywin32/blob/main/win32/src/PythonService.cpp>

これをexeにビルドしたものがPythonService.exeがサービス・アプリの実体になり、サービス開始/停止時などに呼ばれます。  
これをpyd(dll)にビルドしたものがservicemanager.pydになり、ユーザーpythonコードから呼ばれます。  
サービス開始/停止時にこれが中継される以上、登録/削除管理もこちらからやるべきで、該当する処理が

win32serviceutil.HandleCommandLine  
<https://github.com/mhammond/pywin32/blob/main/win32/Lib/win32serviceutil.py>

になります。

[code]

    C:\...\>python プログラム名.py install
    
[/code]

などで登録でき、update/removeもあります。登録できたらあとはscコマンドなどを使えばサービスが実行できます。

[code]

    C:\...\>sc start サービス名
    
[/code]

実際に実行されるサービスはpythonservice.exeになります。登録された情報からユーザープログラムのクラスをロードして、所定のタイミングで呼んでくれますが、python自体はpythonservice.exeに埋め込まれたものを使用しており、実体としてはpythonXXX.dll(pywin32インストール時に決まる)の機能で動いています。なのでvenv用のsys.path設定が十分でなく、ユーザープログラムのロード中にimportなどでコケてしまいます(サービス開始に失敗したときはイベントログにちゃんとエラーが出ます)。

## PythonスクリプトでWindows Service

上記を元にvenv環境でpywin32を使用してWindowsのServiceを作ってみました。

### venvで環境構築

[code]

    C:\...\>python -m venv env
    
[/code]

### venv環境に入る/出る

[code]

    C:\...\>.\env\Scripts\activate
    (env)C:\...\>
    作業
    (env)C:\...\>deactivate
    C:\...\>
    
[/code]

### pipでパッケージインストール

venv環境に入ってから

[code]

    C:\...\>pip install -U pip setuptools
    C:\...\>pip install pywin32
    
[/code]

### hoge.py

(カレントディレクトリに作成)

[code]

    import sys, os, logging, time
    curdir = os.path.dirname(__file__)
    logger = logging.getLogger('hogeservice')
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    filehandler = logging.FileHandler(os.path.join(curdir, 'hogeservice.log'), encoding='utf8')
    filehandler.setFormatter(formatter)
    filehandler.setLevel(logging.DEBUG)
    logger.addHandler(filehandler)
    sys.path.append(os.path.join(os.path.dirname(__file__), 'env\\lib\\site-packages\\win32\\lib'))
    import servicemanager, win32serviceutil, win32service
    
    class HogeService(win32serviceutil.ServiceFramework):
        _svc_name_ = 'HogeService'
        _svc_display_name_ = 'ほげサービス'
    
        def __init__(self, args):
            logger.info('サービス初期化')
            self.stopRequested = False
            win32serviceutil.ServiceFramework.__init__(self, args)
    
        def SvcStop(self):
            logger.info('サービス停止要求')
            self.stopRequested = True
            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
    
        def SvcDoRun(self):
            logger.info('実行開始')
            while not self.stopRequested:
                logger.info('実行中！')
                time.sleep(1)
            logger.info('実行終了')
    
    if __name__ == '__main__':
        logger.info(f'プログラム開始{sys.argv}')
        win32serviceutil.HandleCommandLine(HogeService)
        logger.info('プログラム終了')
    
[/code]

### サービス登録/実行/開始/削除

管理者でコマンドプロンプト実行

[code]

    C:\...\>.\env\Scripts\activate
    (env)C:\...\>python hoge.py install
    Installing service HogeService
    copying host exe 'C:\Users\user\python\asservice\tmp\env\lib\site-packages\win32\pythonservice.exe' -> 'C:\Users\user\python\asservice\tmp\env\pythonservice.exe'
    Service installed
    
    (env)C:\...\>sc query hogeservice
    
    SERVICE_NAME: hogeservice
            TYPE               : 10  WIN32_OWN_PROCESS
            STATE              : 1  STOPPED
            WIN32_EXIT_CODE    : 1077  (0x435)
            SERVICE_EXIT_CODE  : 0  (0x0)
            CHECKPOINT         : 0x0
            WAIT_HINT          : 0x0
    
    (env)C:\...\>sc start hogeservice
    
    SERVICE_NAME: hogeservice
            TYPE               : 10  WIN32_OWN_PROCESS
            STATE              : 2  START_PENDING
                                    (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
            WIN32_EXIT_CODE    : 0  (0x0)
            SERVICE_EXIT_CODE  : 0  (0x0)
            CHECKPOINT         : 0x0
            WAIT_HINT          : 0x7d0
            PID                : 13920
            FLAGS              :
    
    (env)C:\...\>sc query hogeservice
    
    SERVICE_NAME: hogeservice
            TYPE               : 10  WIN32_OWN_PROCESS
            STATE              : 4  RUNNING
                                    (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
            WIN32_EXIT_CODE    : 0  (0x0)
            SERVICE_EXIT_CODE  : 0  (0x0)
            CHECKPOINT         : 0x0
            WAIT_HINT          : 0x0
    
    (env)C:\...\>sc stop hogeservice
    
    SERVICE_NAME: hogeservice
            TYPE               : 10  WIN32_OWN_PROCESS
            STATE              : 3  STOP_PENDING
                                    (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
            WIN32_EXIT_CODE    : 0  (0x0)
            SERVICE_EXIT_CODE  : 0  (0x0)
            CHECKPOINT         : 0x1
            WAIT_HINT          : 0x1388
    
    (env)C:\...\>sc query hogeservice
    
    SERVICE_NAME: hogeservice
            TYPE               : 10  WIN32_OWN_PROCESS
            STATE              : 1  STOPPED
            WIN32_EXIT_CODE    : 0  (0x0)
            SERVICE_EXIT_CODE  : 0  (0x0)
            CHECKPOINT         : 0x0
            WAIT_HINT          : 0x0
    
    (env)C:\...\>python hoge.py remove
    Removing service HogeService
    Service removed
    
    (env)C:\...\>sc query hogeservice
    [SC] EnumQueryServicesStatus:OpenService FAILED 1060:
    
    指定されたサービスはインストールされたサービスとして存在しません。
    
    (env)C:\...\>deactivate
    C:\...\>
    
[/code]

[code]

    2023-03-15 23:18:07,714 - INFO - プログラム開始['hoge.py', 'install']
    2023-03-15 23:18:07,752 - INFO - プログラム終了
    2023-03-15 23:18:25,364 - INFO - サービス初期化
    2023-03-15 23:18:25,365 - INFO - 実行開始
    2023-03-15 23:18:25,365 - INFO - 実行中！
    2023-03-15 23:18:26,366 - INFO - 実行中！
    2023-03-15 23:18:27,366 - INFO - 実行中！
    2023-03-15 23:18:28,367 - INFO - 実行中！
    2023-03-15 23:18:29,367 - INFO - 実行中！
    2023-03-15 23:18:30,368 - INFO - 実行中！
    2023-03-15 23:18:31,368 - INFO - 実行中！
    2023-03-15 23:18:32,048 - INFO - サービス停止要求
    2023-03-15 23:18:32,369 - INFO - 実行終了
    2023-03-15 23:19:03,637 - INFO - プログラム開始['hoge.py', 'remove']
    2023-03-15 23:19:03,674 - INFO - プログラム終了
    
[/code]

### 質問に関する補足

原因はvenvに関連したsys.path設定(pythonモジュール検索パス設定)だと思います。  
ただしpyenvは使っていません。バージョンを切り替えるだけで動かなくなる可能性があります。

### 感想

pythonでWindowsサービスなんて、自分で軽く使いたいときくらいだと思うので、exe化なんて考えられません。実際どんどん書き換えられるので便利ですが。。。

誰かに頼まれてもお金をもらっても、もらった.exeは絶対実行しないように！


CC BY-SA 4.0
2023-03-15T14:57:18.650
94194
2023-03-15T14:57:18.650
54957
94163
answer