ようやくWindowsサービスをRubyで作れるレベルになったので、纏めてみる。
まず、WindowsサービスをRubyで作成する上で必要なライブラリWin32-Serviceを入手する。
RubyGemsを使用して入手するのが一番楽だと思われる。
Win32アプリケーションを作成するにあたって、Win32Utilsはかなり重要になってくる(と思われる)ので、他にも必要としているものが無いか探しておくと、後で悩むことが無くなって良いと思う。
RubyGemsとWin32-Serviceのインストール
http://projectzero-swb.blogspot.com/2009/08/win32-servicerubygems.html
RubyでWindowsサービスを作成する上で必要なことは、
・サービス登録を行うスクリプトの作成
・サービスデーモンの作成
・サービスデーモンに行わせる処理の作成
の3つ。
この中で、・サービス登録を行うスクリプトの作成は、Windows標準付属のsc.exeで代用することも可能だが、そこも自動化できないといろいろ面倒なので、普通にスクリプトを作成する。
作ると言っても、殆どライブラリ付属のサンプルスクリプトのままでおkなのだが…
以下がサービスを管理するクラス。
定数SERVICE_NAME、SERVICE_DISPLAYNAME、SERVICE_DESCRIPTION、SERVICE_COMMANDを編集することによって、どんなサービスにも応用できる…はず。
サービスを登録…service_install
サービスを開始…service_start
サービスを停止…service_stop
サービスを再開…service_resume
サービスを削除…service_uninstall
サービスの状態を返す…service_stat
#! ruby.exe
require 'rubygems'
require 'win32/service'
include Win32
# サービスの作成に必要な情報を定義(ServiceSetupクラスで使用)
SERVICE_NAME = "Rubyservice" # サービスの正式な名前。これで全て管理される。
SERVICE_DISPLAYNAME = "RubyService" # サービスの表示名。サービス一覧などに表示される名前
SERVICE_DESCRIPTION = "Rubyで作成したサービスです。" #
# Rubyスクリプトそのままだと、インタプリタのパス指定間違いによる193 0xc1エラーが発生するなど、
# いろいろ面倒なのでexerbでWindows用exeファイル化しておく
SERVICE_COMMAND = "C:\\Service\\Rubyservice.exe"
class ServiceSetup
def initialize
raise TypeError if SERVICE_NAME == nil || SERVICE_NAME.class != String
raise TypeError if SERVICE_DISPLAYNAME == nil || SERVICE_DISPLAYNAME.class != String
raise TypeError if SERVICE_DESCRIPTION == nil || SERVICE_DESCRIPTION.class != String
raise TypeError if SERVICE_COMMAND == nil || SERVICE_COMMAND.class != String
end
# サービスインストール
def service_install
Service.new(
:service_name => SERVICE_NAME,
:display_name => SERVICE_DISPLAYNAME,
:description => SERVICE_DESCRIPTION,
:binary_path_name => SERVICE_COMMAND
)
sleep 1
puts 'サービス ' + SERVICE_NAME + ' をインストールしました。'
end
# サービス開始
def service_start
if Service.status(SERVICE_NAME).current_state != 'running'
Service.start(SERVICE_NAME)
while Service.status(SERVICE_NAME).current_state != 'running'
puts 'One moment...' + Service.status(SERVICE_NAME).current_state
sleep 1
end
puts 'サービス ' + SERVICE_NAME + ' を開始しました。'
else
puts SERVICE_NAME + " は既に実行されています。"
end
end
# サービス停止
def service_stop
if Service.status(SERVICE_NAME).current_state != 'paused'
Service.pause(SERVICE_NAME)
while Service.status(SERVICE_NAME).current_state != 'paused'
puts 'One moment...' + Service.status(SERVICE_NAME).current_state
sleep 1
end
puts 'サービス ' + SERVICE_NAME + ' を停止しました。'
else
puts SERVICE_NAME + ' は既に停止しています。'
end
end
# サービス再開
def service_resume
if Service.status(SERVICE_NAME).current_state != 'running'
Service.resume(SERVICE_NAME)
while Service.status(SERVICE_NAME).current_state != 'running'
puts 'One moment...' + Service.status(SERVICE_NAME).current_state
sleep 1
end
puts 'サービス ' + SERVICE_NAME + ' を再開しました。'
else
puts SERVICE_NAME + " は既に実行されています。"
end
end
# サービスアンインストール
def service_uninstall
if Service.status(SERVICE_NAME).current_state != 'stopped'
Service.stop(SERVICE_NAME)
end
while Service.status(SERVICE_NAME).current_state != 'stopped'
puts 'One moment...' + Service.status(SERVICE_NAME).current_state
sleep 1
end
Service.delete(SERVICE_NAME)
puts 'サービス ' + SERVICE_NAME + ' を削除しました。'
end
# サービスの状態を返す
def service_stat
begin
stat = Service.status(SERVICE_NAME).current_state
rescue
return false
end
return stat
end
end
次に、サービスデーモンを作成する。
ここでは例として、DDNSUpdaterのおまけ(?)として作っていた「みくろっく」をWindowsサービスデーモン化してみた。
Win32::Daemonクラスには、
サービスの開始時に実行…service_init
サービスのメイン処理…service_main
サービス停止時の処理…service_stop
サービス一時停止時の処理…service_pause
サービスらい開示の処理…service_resume
を定義することができる。
それぞれのメソッドは、対応するシグナルが送られてきた時に実行される。
通常は、
service_init→service_main
の順で実行される。
#! ruby.exe
begin
require 'rubygems'
require 'win32/daemon'
# require 'win32/sound' # サービスにすると音が出なかったので
include Win32
SYSTEM_BINDIR = ENV["ProgramFiles"] + "\\Mikulock"
SYSTEM_WAVDIR = SYSTEM_BINDIR + "\\Sound"
SYSTEM_LOGDIR = ENV["ProgramData"] + "\\Mikulock"
SYSTEM_BINARY = SYSTEM_BINDIR + "\\mikulock_service.exe"
SYSTEM_LOG = SYSTEM_LOGDIR + "\\mikulock.log"
#Win32::Daemonクラスの継承
class RubyDaemon < Daemon
# サービスの初期化
def service_init
File.open(SYSTEM_LOG, "a") {|writelog|
writelog.printf("%-36s - Service Start.\n", Time.now.to_s)
}
sleep 10
end
module Mikulock
def self.mikulock_play(play)
if File.exist?("Sound\\" + play) == true
IO.popen("mpg123 -q Sound\\#{play}")
File.open(SYSTEM_LOG, "a") do |writelog|
writelog.printf("%-36s - Play: %s\n", Time.now.to_s, File.expand_path("Sound\\" + play))
end
end
end
def self.mikulock_timecheck
if Time.now.to_a[1] == 0
case Time.now.to_a[2]
when 0
return "0h.mp3"
when 1, 13
return "1h.mp3"
when 2, 14
return "2h.mp3"
when 3, 15
return "3h.mp3"
when 4, 16
return "4h.mp3"
when 5, 17
return "5h.mp3"
when 6, 18
return "5h.mp3"
when 7, 19
return "7h.mp3"
when 8, 20
return "8h.mp3"
when 9, 21
return "9h.mp3"
when 10, 22
return "10h.mp3"
when 11, 23
return "11h.mp3"
when 12
return "12h.mp3"
end
elsif Time.now.to_a[1] == 30
return "30min.mp3"
else
return false
end
end
end
# サービスのメイン処理
def service_main
play = String.new
play_end = String.new #お知らせが終わったものを記録
# 最初の一回を設定
if Time.now.to_a[2] >= 5 && Time.now.to_a[2] < 11
play = "morning.mp3"
elsif Time.now.to_a[2] >= 11 && Time.now.to_a[2] < 18
play = "noon.mp3"
else
play = "night.mp3"
end
# 音声を再生する為のスレッド
thread = Thread.new do
while running?
if state == RUNNING
if play != false && play != play_end
Mikulock.mikulock_play(play)
play_end = play
end
sleep 5
end
end
end
# サービスの状態がrunningの時のみ繰り返す
while running?
if state == RUNNING
play = Mikulock.mikulock_timecheck
File.open(SYSTEM_LOG, "a") {|writelog|
writelog.printf("%-36s - Running\n", Time.now.to_s)
}
sleep 5
else
sleep 1
end
end
end
# Stop(停止)シグナルが送られてきた時の処理
def service_stop
File.open(SYSTEM_LOG, "a"){|writelog|
writelog.printf("%-36s - Stop\n", Time.now.to_s)
}
end
# Pause(一時停止)シグナルが送られてきた時の処理
def service_pause
File.open(SYSTEM_LOG, "a"){|writelog|
writelog.printf("%-36s - Pause\n", Time.now.to_s)
}
end
# Resume(再開)シグナルが送られてきたときの処理
def service_resume
File.open(SYSTEM_LOG, "a"){|writelog|
writelog.printf("%-36s - Resume\n", Time.now.to_s)
}
end
end
# mpg123のインストール先にcdしておく
Dir.chdir(SYSTEM_BINDIR + "\\mpg123"){
# このループの中で全ての処理が行われる
RubyDaemon.mainloop
}
# 例外が発生した時は、例外の内容をログに書き込み、さらに例外を発生させる
rescue => error
File.open(SYSTEM_LOG, "a"){|writelog|
writelog.printf("%-36s - Error: %s\n", Time.now.to_s, error)
}
raise
end
これをそのまま実行しても、`mainloop': Service_Main thread exited abnormally.....となり、実行できないので、
サービス登録クラスを組み込んだスクリプトを利用し、サービスに登録、ログを読みながらデバッグする必要がある。
サービス作成中に良くあるミスは、
・Rubyインタプリタのパス、プログラムのソースのパスが間違っている
・service_mainのwhile running?の中が正しく書かれていない
・while running?の中にsleep 60などと書いてしまい、60秒待たないとサービスを停止できなくなる
・例外処理が不十分でサービスが落ちる
だった。
サービスの登録が終わったら、
コントロールパネル→管理ツール→サービス
を開き、登録したサービスが正しく実行されているか確認する。
サービスの状態が「開始」になっていれば、問題無く動作している…はず。
OS起動時に自動実行したいときは、「サービス」でMikulockのプロパティを開き、
「スタートアップの種類」を「自動」もしくは「自動(遅延開始)」に設定すれば良い。
09.08.04 追記
・スクリプトの中で「スタートアップの種類」を「自動」に設定するにはどうしたら…?
:start_type => Service::AUTO_START
を、サービスインストール時の引数に渡せば良い模様。
ここまでで、とりあえずWindowsサービスを利用したRubyアプリケーションが作れるということが分かった。
以前はWin32/Soundを使用し、音を鳴らしていたが、サービスにすると上手く鳴らないようだったので、
mpg123を使用し、mp3を再生するようにしてみたのだが、
mpg123の-qオプションを使用しても、なにかが標準入力か標準出力を開くようで、
「対話型サービスの検出」ウィンドウが現れてしまうという問題が…
「デスクトップとの対話をサービスに許可する」を無効にすれば一応出現しなくなるが、Windowsのアプリケーションログに残ってしまうので、根本的な解決にはならない。
更に、mpg123には、どんなmp3ファイルを再生しても最後の2〜3秒がカットされてしまうという問題も…
今回作ったみくろっく(Windowsサービス版)のダウンロードは以下から。
Win32-Serviceのテスト用途にどうぞ。
※09.09.17 追記
みくろっく改良版
※09.08.07 追記
Dynamic DNS UpdaterをWindowsサービス化してみた。
以下からダウンロードできます。
0 件のコメント:
コメントを投稿