読者です 読者をやめる 読者になる 読者になる

Keep on moving

あんまりまとまってないことを書きますよ

pythonでのWindowsサービスの書き方

python

初めに

Pythonのスクリプトを書いていて、常駐化させたい時があります。

*nix系だとpython-daemon 1.5.5 : Python Package Indexを使って実現できるようですが、
Windowsのサービスは探してもあまり情報が見当たらないので腹を据えて調べてみました。

なんだかんだでwin32APIの知識が必要になって割と骨だったのですが、
ある程度動くものができたので、公開します。

[注意] 私はwin32APIの知識が足りません。おかしなところがあればご指摘をお願いします。

基礎知識

参考によるとpywin32モジュールのwin32serviceutilを使って実現できるようです。

これからお見せする例ではWindowsのイベントハンドル周りの知識が必要ですが、
身構える必要はありません。
サービスを作る分には以下のwin32APIをおさえておけばokです。

  • CreateEvent() : イベントハンドルを作成(一般的には非シグナル[フラグオフ状態]で作成)
  • SetEvent() : イベントハンドルをシグナル化[フラグオン状態に変更]
  • WaitForSingleObject() : イベントをハンドルがシグナル化するまで待つ

なにもしないWindowsサービス

初めに何もしないWindowsサービスを作成してみます。
サービスの開始/停止のみ可能です。

# coding: utf-8

import win32service
import win32serviceutil
import win32event

# win32serviceutil.ServiceFrameworkを継承する
class SmallestPythonService(win32serviceutil.ServiceFramework):
    """
    管理ツール => サービス画面への表示内容
    """
    # サービス名
    _svc_name_ = "SmallestPythonService"
    # 表示名(サービス画面にはこれを表示)
    _svc_display_name_ = "Display Service"
    # サービスの説明
    _svc_description_='do nothing speical'

    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):
    # SvcStopメソッド内でイベントハンドルがシグナルするまで待機
	win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)

    # 待機関数がWAIT_OBJECT_0などを返した場合非シグナル状態に。

if __name__=='__main__':
    # コマンドラインから登録する
    win32serviceutil.HandleCommandLine(SmallestPythonService)

これをserver.pyとして保存し、以下のように実行してサービスとして登録します。

サービスの登録

> python server.py install

実行後はサービス画面から開始/停止などの設定が可能です。

f:id:Ehren:20110511175342j:image

ちなみに削除するときは以下のようにします。

> python server.py remove

他にも機能はいろいろあります

Usage: 'service.py [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] : 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.

サービスからのファイル出力

なにもしないのもさみしいので、ファイルへ出力するサービスを書いてみます。
SvcStopメソッド内でイベントハンドルがシグナルするまで待機していたのを、タイムアウト時間を設定し、
処理を行うように変更します。

# coding: utf-8
import win32service
import win32serviceutil
import win32event
import datetime

class SmallestPythonService(win32serviceutil.ServiceFramework):
    _svc_name_ = "SmallestPythonService"
    _svc_display_name_ = "Display Service"
    _svc_description_='do nothing speical'
    # シグナルまでの待ち用タイムアウト時間(今回は10秒 = 10,000ミリ秒)
    _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):
	while 1:
            # イベントのシグナル化を10秒待つ
	    ret = win32event.WaitForSingleObject(
			    self.hWaitStop,
			    self._timeout_Milliseconds
			    )
            # サービス停止(イベントがシグナル化)ならば、処理を中止
	    if ret == win32event.WAIT_OBJECT_0:
		break
	    self.DoRun()

    # 実際のサービスの処理
    def DoRun(self):
	with open("c:\\temp\\test.txt", "a") as f:
	    f.write("[Test] %s \n" % (datetime.datetime.now()))

if __name__=='__main__':
    win32serviceutil.HandleCommandLine(SmallestPythonService)

で、やっと本題

定期的にatndからデータを取ってきて結果をGrowl on Windowsに出力するスクリプトをサービスとして常駐化してみます。
Pythonから Growl on Windowsに投げるのはGrowl for Linuxを使ってみた | hexacosa.netを参考にしました。

# coding: utf-8
import gntp
from gntp.notifier import GrowlNotifier

import urllib2
import json

import win32service
import win32serviceutil
import win32event
import datetime

class SmallestPythonService(win32serviceutil.ServiceFramework):
    _svc_name_ = "SmallestPythonService"
    _svc_display_name_ = "Display Service"
    _svc_description_='do nothing speical'

    # 30分ごとに実行
    _timeout_Milliseconds = 1000 * 30

    def __init__(self, args):

        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
        # growlの設定
	self.notifier = GrowlNotifier("python script", ["TEST"], hostname="127.0.0.1")
	#self.notifier.register() # 一回だけ実行

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.hWaitStop)

    def SvcDoRun(self):
	while 1:
	    ret = win32event.WaitForSingleObject(
			    self.hWaitStop,
			    self._timeout_Milliseconds
			    )

	    if ret == win32event.WAIT_OBJECT_0:
		break
	    self.DoRun()

    def DoRun(self):	    
	URL = 'http://api.atnd.org/events/?event_id=13124&format=json'

        r = urllib2.urlopen(URL)
	jj = json.load(r)
        info = jj["events"][0]
	t = info["title"]
	a = info["accepted"]
	w = info["waiting"]
	l = info["limit"]
	self.notifier.notify("TEST", "atnd info", "%s \n %s/%s (wait:%s)" % (t,a, l, w))

if __name__=='__main__':
    win32serviceutil.HandleCommandLine(SmallestPythonService)

30分ごとにイベントの参加者数が表示されます。
f:id:Ehren:20110511183753j:image

お疲れ様でした。

注意

今回はpywin32をインストーラーで入れたためか、buildoutでつくった環境のeggファイルが読み込み
できませんでした。サービス化するにはeasy_installするしかないのかも。

まとめ

今回紹介したようにpythonのスクリプトを常駐化するのは割と簡単です。
これを応用すると、Redmineのチケット状態を監視して、変更されたときにskype
書き込むような処理も常駐化できます。

久々にWin32APIを調べることになったりいろいろありましたが、最初の目標を達成できました。
先人に感謝。