2009年8月3日月曜日

RubyでWindowsのサービスを作る

以前DDNSUpdaterを作った時に、「Windowsサービス化はむりぽ」と書いたが、
ようやく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 件のコメント: