Ruby 2.0 + Sinatra + Thin を Windows サービス化してみた

簡単な Web アプリを Ruby + Sinatra で作って、それを Windows サービス化しようとしたらすごくハマったのでメモしておく。 最終的には Thin を使って Winsw で Windows サービス化した。

リポジトリhttps://github.com/toruuetani/SinatraThinService

Ruby は 2.0 から使い始めたのでほとんど知らない。おかしいところがあれば指摘してください。

前提

Ruby 製 HTTP サーバー

とりあえず検索してみたところ有名なのはこれくらいらしい。

WEBrick は開発向けなので除外するととして、 Passenger と Unicorn は Windows で動かないので問題外。 そうなると Mongrel と Thin しか候補がないんだけども、 Mongrel はすでに開発が止まっているらしい。 実際 Ruby 1.9 には正式に対応しておらず、開発版でなんとか動く状況のようだ。 Ruby 2.0 ではほぼ情報がなく、開発版でも動作しなかったので Thin しか望みがない。

ただし、 Mongrel では mongrel_service というそのものずばりの gem があったんだけども Thin には Windows サービス化する方法がない。

winsw: Windows Service Wrapper

そこで kohsuke/winsw という実行モジュールを Windows サービス化する ユーティリティを使うことにした。 Jenkins でも使われているものなので、信頼性も十分だろう。

ただ困ったことに Bundler のように子プロセスを起動する場合には対応しておらず、サービスを停止しても子プロセスが 停止してくれない。モダンな Ruby 環境であるなら Bundler ははずせないだろう、と思ったのがいろいろハマった原因の一つ。

起動自体は問題ないが、停止できないのは気持ち悪いので少し変更して使うことにした。

Sinatra の準備

ここからは実際に動かす方法を記述していく。

RubyC:\Ruby200 にインストールされているものとして Bundler をインストールしておくこと。

gem install bundler

Bundler をインストールしたら作業ディレクトリで以下のコマンドを実行する。

※ thin の依存ライブラリ eventmachine のコンパイルに必要なので、 DevKit と gcc をインストールしておくこと。

bundle init
#Modify Gemfile
bundle install --path vendor/bundle
# A sample Gemfile
source "https://rubygems.org"

gem "sinatra"
group :development do
    gem "sinatra-contrib"
    gem "better_errors"
    gem "binding_of_caller"
end
gem "thin"

最低限 Sinatra が動くコードを用意する。

# -*- encoding: utf-8 -*-
# app.rb
require "rubygems"
require "sinatra"

get "/" do
    "Hello"
end

bundle exec ruby app.rbhttp://localhost:4567 が動くことを確認する。 または config.ru を用意して bundle exec rackuphttp://localhost:9292 が動くことを確認する。

# -*- encoding: utf-8 -*-
# config.ru
require "./app.rb"


run Sinatra::Application

winsw の準備

現時点での最新(1.13)だと子プロセスを停止できないので、 Bundler 経由だと winsw を停止しても Thin が停止できない。

winsw.exe -> ruby(Bundler) -> ruby(Thin) となるので winsw.exe を停止しても ruby(Bundler) しか停止されない。

なのでパッチを当てたものを使う(上記リポジトリに含まれる SinatraThinService.exe )。

winsw.exe の設定は winsw.xml で行う。名前は任意のものに変更可能。ポイントは次の通り。

  • Bundler で使う Gemfile環境変数 BUNDLE_GEMFILE で指定する。
  • workingdirectory を指定する。
<service>
  <id>SinatraThinService</id>
  <name>SinatraThinService</name>
  <description>Sinatra with Thin Service</description>
  <env name="PATH" value="C:\Ruby200\bin;%PATH%;"/>
  <env name="BUNDLE_GEMFILE" value="C:\Documents\Work\SinatraThinService\Gemfile"/>
  <executable>ruby</executable>
  <arguments>C:\Ruby200\bin\bundle exec ruby .\vendor\bundle\ruby\2.0.0\bin\thin -R .\config.ru start</arguments>
  <workingdirectory>C:\Documents\Work\SinatraThinService</workingdirectory>
  <logmode>rotate</logmode>
</service>

サービスの使い方

管理者権限でコマンドプロンプトを起動し、以下のコマンドを実行する。これで http://localhost:3000 でアクセスできるか確認する。

SinatraThinService install
SinatraThinService start

停止するときは

SinatraThinService stop

アンインストールは次の通り。

SinatraThinService uninstall

Chef SoloでTracLightningぽい環境を構築してみた

Chefが面白そうなのでいろいろやってみた結果、TracLightning+hglightぽい環境を構築できるようになったのでまとめを書いてみることにした。TracLightningと違うのはSubversion/MavenがないとかTracプラグインが最小限しかないくらいかな。

f:id:re_guzy:20130525220055p:plain

動作確認は次の環境で行った。

  • Chef 11.4.0
  • Windows 7 SP1 Professional(x86)
  • Windows Server 2008 R2 Standard
  • Windwos Server 2012 Standard

Chefリポジトリここで、cookbook名はtracwithhgとした。

tracwithhgがやっていること

  1. Visual C++ 2008 ランタイムのダウンロードとインストール
    • Python の動作に必要なため
  2. Python 2.7.4のダウンロードと"C:/Trac/python"への展開
    • easy_install のインストールも
  3. Mercurial 2.5.4のダウンロードとインストール
    • attribute(node['hg']['need_build'])にtrueを指定すると、MinGWをインストールして、ソースからビルド後インストールする。
  4. Trac 1.0.1のダウンロードとインストール
    • Wikiに関しては TracJa 1.0を使っている。
    • プロジェクト作成時に wiki load してるだけ。
  5. TracAccountManagerPlugin 0.4.3のダウンロードとインストール
    • 認証方式にDigestを指定した場合のみ
    • 認証方式にSSPIを指定した場合はインストールされない
  6. TracMercurialPlugin 1.0.0.3のダウンロードとインストール
  7. Apache 2.2.24のダウンロードと"C:/Trac/apache"への展開
  8. ファイアーウォールの例外にapache追加
  9. JRE 7u21のダウンロードと"C:/Trac/jre"への展開
  10. .NET Framework 3.5 SP1 の有効化
    • JenkinsのWindowsサービス化に必要
    • Windows 8/Windows Server 2012 ではデフォルトで無効化されているため必要
  11. Jenkins 1.514のダウンロードと"C:/Trac/jenkins"への展開&Trac(Jenkins)のWindowsサービス化
  12. Trac(Apache)のWindowsサービス化
    • Twitter Bootstrapで作成したHTMLをApacheで公開
    • mod_wsgi 3.3のダウンロードと有効化
    • SSPI認証なら mod_auth_sspi 1.0.4のダウンロードと有効化
    • Tracをmod_Wsgiで公開できるようにhttpd.conf書き換え

準備

  1. Installing Chef Client on Windows - Chef - Opscode Open Source WikiからChefのインストーラをダウンロードしてインストール
  2. toruuetani / Chef-repository-TracWithHg ― Bitbucket を任意の場所(「C:/Chef」とか)にclone
  3. Chefに同梱されている「win32/registry.rb」を修正(参照:Chef Solo で invalid byte sequence in Windows-31J / invalid byte sequence in UTF8 に遭遇したときの対処をいくつか - 記憶は削除の方向で

実行方法

cloneしてきたChefリポジトリに同梱されている「install.bat」を管理者権限で実行するだけ。
※Windows Server 2012 の場合、インストールメディアがDドライブにセットされている必要あり。Dドライブ以外にインストールメディアをセットする場合、attribute(node['netfx3']['source'])を環境に合わせて修正すること。

プロジェクト作成方法

create-hg-project.batを実行するだけ。
最低限Tracプロジェクト名を指定する必要あり。Mercurialリポジトリをcloneする場合はcloneするURLを指定すること。

既存プロジェクトに管理するMercurialリポジトリを追加する場合、create-hg-append.batを実行してTracプロジェクト名とMercurialリポジトリ名を指定する。Mercurialリポジトリをcloneする場合はcloneするURLを指定すること。

Digest認証の場合、初期ユーザーとして次の2アカウントを用意する。

  • アカウント: admin, パスワード: admin
  • アカウント: guest, パスワード: guest

TODO

  • Jenkinsでプロジェクト作成
  • Jenkinsでバックアップ
  • Jenkins関連ができたらhglightにも反映すること

Chef Solo で invalid byte sequence in Windows-31J / invalid byte sequence in UTF8 に遭遇したときの対処をいくつか

Chef-solo に同梱されている Ruby は 1.9.3p286 なので、エンコーディングが 1.8.x に比べて改善されているらしい。が、やはり日本語 Windows を使ってるといろいろハマるのでメモ。

前提

環境変数 LANG がセットされていない場合、

ECHO %LANG%
=> (空)

外部エンコーディングは Windows-31J

ruby -e "p Encoding.default_external"
=> #<Encoding:Windows-31J>

最近の風潮としてエンコーディングの基本はUTF8なので、環境変数 LANG をセットしておく。

SET LANG=ja_JP.utf8

そうすると外部エンコーディングも UTF8 になる。

ruby -e "p Encoding.default_external"
=> #<Encoding:UTF-8>

で、ソースコードはマジックコメントでUTF8を指定しておく。

# -*- encoding: utf-8 -*-

基本はこれで問題ないんだけども、 日本語 Windows では Windows-31J と付き合わないわけにはいかないのでまれに問題がおきる。

windows_batch リソース

何らかのコマンドに対して日本語で引数を指定する場合次のように書くわけだが、受け側ではShiftJISを期待してる。
ところがマジックコメントでUTF8を指定しているため、受け側には文字化けした文字列がわたってしまう。

windows_batch "sample" do
    code = <<-EOH
    hoge 日本語
    EOH
end

なのでコマンドに日本語を渡したい場合は Windows-31Jエンコードしましょう。

windows_batch "sample" do
    tmp = <<-EOH
    hoge 日本語
    EOH
    code tmp.encode('Windows-31J', 'utf-8')
end

windows_package リソース

「アプリケーションの追加と削除」に登録されるようなアプリケーションをインストールしてくれる便利なコマンドなんだけど、やっぱり日本語が入るとエラーが発生します。
難点なのが、目当てのアプリケーションに日本語が入ってなくても、アプリケーションがインストールされているかのチェックでレジストリを参照すること。このときに日本語を含むアプリケーションがあるとエラーが発生してしまう。

windows_package "Fuga Application" do
    source "path/to/fuga"
    installer_type :customoptions "/q"
    action :install
end

これはレシピをいじっても仕方なくて、同梱されている Ruby ライブラリの問題。具体的には win32/registry.rb 。これを以下のように修正してあげればいいい。

+++ C:/opscode/chef/embedded/lib/ruby/1.9.1/win32/registry.rb	Thu May 09 11:12:35 2013
@@ -165,11 +165,14 @@
         dlload "kernel32.dll"
       end
       FormatMessageA = Kernel32.extern "int FormatMessageA(int, void *, int, int, void *, int, void *)", :stdcall
+      FormatMessageW = Kernel32.extern "int FormatMessageW(int, void *, int, int, void *, int, void *)", :stdcall
       def initialize(code)
         @code = code
         msg = "\0".force_encoding(Encoding::ASCII_8BIT) * 1024
-        len = FormatMessageA.call(0x1200, 0, code, 0, msg, 1024, 0)
-        msg = msg[0, len].force_encoding(Encoding.find(Encoding.locale_charmap))
+        #len = FormatMessageA.call(0x1200, 0, code, 0, msg, 1024, 0)
+        len = FormatMessageW.call(0x1200, 0, code, 0, msg, 1024, 0) * 2
+        #msg = msg[0, len].force_encoding(Encoding.find(Encoding.locale_charmap))
+        msg = msg[0, len].force_encoding("UTF-16LE").encode(Encoding.find(Encoding.locale_charmap))
         super msg.tr("\r", '').chomp
       end
       attr_reader :code

Chef-soloでプロキシ設定の切り替え

今までサーバー設定なんかは Fabric でいろいろやってきたわけなんですが、 Trac をホストしてた Windows サーバーのリプレースをする必要がでてきたので、最近注目してた Chef でいろいろやってみました。

が、 Ruby 初心者にはいろいろ難しく、なんといってもあまり Windows 向けではないので、結構よく詰まります。なので備忘録を兼ねてちょっとずつメモしていく予定。


Chef-solo の場合、 solo.rb という設定ファイルにツール設定をしていくんだけども、大抵はファイルキャッシュとクックブックの場所を設定するだけでいい。

# solo.rb
# -*- encoding: utf-8 -*-
file_cache_path File.join(Dir.pwd, 'cache')
cookbook_path   File.join(Dir.pwd, 'cookbooks')

普段はこれで問題ないんだけども、職場などプロキシ環境下にいるときは設定を追加しないといけない。

# solo.rb
# -*- encoding: utf-8 -*-
file_cache_path   File.join(Dir.pwd, 'cache')
cookbook_path     File.join(Dir.pwd, 'cookbooks')
http_proxy        "http://path/to/proxy:8080"
https_proxy proxy "http://path/to/proxy:8080"

別のマシンをセットアップするならともかく、1台のマシンをセットアップするのに別の設定ファイルを用意するのはなんか違う、ということで自動的にプロキシの設定ありなしを判断するようにしてみたのがこれ。

# -*- encoding: utf-8 -*-
file_cache_path File.join(Dir.pwd, 'cache')
cookbook_path   File.join(Dir.pwd, 'cookbooks')


require('win32/registry')
key = 'Software\Microsoft\Windows\CurrentVersion\Internet Settings'
reg = Win32::Registry::HKEY_CURRENT_USER.open(key)
if reg["ProxyEnable"] == 1
    proxy = "http://" + reg["ProxyServer"]
    http_proxy  proxy
    https_proxy proxy

    ENV['http_proxy'] = proxy
end

単純にIEなどで使われるインターネット接続設定をレジストリから取得してるだけ。環境変数ENVに[http_proxy]をセットしてるのは、ほかのツールなどで使われることを想定するため。これでどこでも開発が進められますね。

SourceTree for Windows で使われているWPFライブラリ

Free Mercurial and Git Client for Windows and Mac | Atlassian SourceTreeをダウンロードしてみたら、全然知らないWPFライブラリが使われてるので、備忘録を兼ねてメモ。

64bit Windows が本格的に普及する前にネイティブ DLL を P/Invoke する .NET アプリがやっておくべきこと

以前からいろいろ調べたり質問したりしてた、ネイティブ DLL を P/Invoke する .NET アプリを 64bit Windows でも動作させる方法がようやく納得できる形で見つかったので、備忘録を兼ねてメモしておく。

P/Invoke したいネイティブDLL

  • x86用DLLとx64用DLLが用意されているネイティブDLL
  • System.Data.SQLite ※正確にはネイティブDLLではなく、ネイティブDLLを P/Invoke するアセンブリ

QA@ITでした質問

参考にしたサイト

なぜ必要か

まだまだ 32bit のサポートが必要だが、現在販売されている個人向けPCは 64bit 向けが大半を占めている。サーバーOSに至っては、 2008R2から 64bit しか用意されていない。

結論

  1. プロジェクトはターゲットプラットフォームを AnyCPU にする。
  2. x86用ネイティブDLLをx86ディレクトリに配置し、ビルドアクションを「コンテンツ」に設定する。さらに出力ディレクトリにコピーするようにしておく。
  3. x64用ネイティブDLLをx64ディレクトリに配置し、同様に設定する。
  4. SetDllDirectory でDLL検索パスを切り分ける。
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Hoge {
    /// <summary>
    /// ネイティブDLLのロード先ディレクトリを追加するユーティリティクラスです。
    /// </summary>
    public static class DllSearchPath {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool SetDllDirectory(string lpPathName);
        /// <summary>
        /// ネイティブDLLのロード先ディレクトリを追加します。
        /// </summary>
        /// <param name="path">ロード先ディレクトリ</param>
        public static void Add(string path) {
            SetDllDirectory(path);
        }
        /// <summary>
        /// x86とx64のネイティブDLLのロード先ディレクトリを追加します。
        /// </summary>
        /// <param name="x86path">x86用ロード先ディレクトリ</param>
        /// <param name="x64path">x64用ロード先ディレクトリ</param>
        public static void Add(string x86path, string x64path) {
            int bitWidth = IntPtr.Size * 8;
            switch (bitWidth) {
                case 64:
                    Add(x64path);
                    break;
                case 32:
                    Add(x86path);
                    break;
                default:
                    throw new NotSupportedException();
            }
        }
    }
}

#Program.cs
DllSearchPath.Add("path/to/x86", "path/to/x64");

ちなみに、 x86向けネイティブDLLしか用意されていない場合、ターゲットプラットフォームを x86 にしておく方が無難だろう。

動作原理

AnyCPU に設定しておけば、64bit Windows では 64bit プロセスとして動作し、32bit Windows では 32bit プロセスとして動作する。
DllSearchPath.Add で 32bit 向けパスと 64bit 向けパスをあらかじめ指定しておけば、自動的にプロセスを判断してロードするDLL
が配置されるディレクトリを決定してくれる。

この方法であれば、わざわざプロジェクトを x86 向けと x64 向けに2つ用意する必要はなくなる。

System.Data.SQLite

参考URLにあるとおり、

  • 32bit ・・・ Precompiled Statically-Linked Binaries for 32-bit Windows
  • 64bit ・・・ Precompiled Statically-Linked Binaries for 64-bit Windows

をダウンロードしておけばいい。選定理由はこんな感じ。

  • Visual C++ 2008 SP1 runtime はstaticリンクの方が無難。当然ランタイム分容量が増えるが、今更気にするサイズではないだろう。
  • mixed-mode assembly はネイティブDLLをアセンブリに含んでしまうので、ターゲットプラットフォームが x86 または x64 になってしまう。参照アセンブリは静的に決まるため、ロードするネイティブDLLを動的に切り分けられない。

コマンドラインから ASP.NET4 を有効化する

最近いろいろ試してみたのでメモ。基本的に Windows 7 SP1 でしか動作確認してない。
以下の操作はすべて管理者権限で実行すること。

IISASP.NETの有効化

Windows コンポーネントなので ocsetup かと思ったが、 DISM コマンドでいけた。ただし DISM コマンドは結構重いので、無効化されてる場合だけ有効化するようにした。

SET FEATURE=IIS-WebServerRole
dism /online /get-features /format:table | findstr /i "%FEATURE%" | find /i "有効"
IF "%ERRORLEVEL%"=="1" dism /online /enable-feature /featurename:"%FEATURE%"

SET FEATURE=IIS-ISAPIFilter
dism /online /get-features /format:table | findstr /i "%FEATURE%" | find /i "有効"
IF "%ERRORLEVEL%"=="1" dism /online /enable-feature /featurename:"%FEATURE%"

SET FEATURE=IIS-ISAPIExtensions
dism /online /get-features /format:table | findstr /i "%FEATURE%" | find /i "有効"
IF "%ERRORLEVEL%"=="1" dism /online /enable-feature /featurename:"%FEATURE%"

SET FEATURE=IIS-NetFxExtensibility
dism /online /get-features /format:table | findstr /i "%FEATURE%" | find /i "有効"
IF "%ERRORLEVEL%"=="1" dism /online /enable-feature /featurename:"%FEATURE%"

SET FEATURE=IIS-ASPNET
dism /online /get-features /format:table | findstr /i "%FEATURE%" | find /i "有効"
IF "%ERRORLEVEL%"=="1" dism /online /enable-feature /featurename:"%FEATURE%"

ASP.NET4の有効化

IIS .NET のインストール順によっては ASP.NET が有効化されない場合がある。
aspnet_regiis -lv で有効化されている ASP.NET のバージョンが表示されるので、それで ASP.NET4 がなければ有効化する。

%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -lv | find /i "4.0.30319.0"
IF "%ERRORLEVEL%"=="1" %SystemRoot%\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -i

上記で ASP.NET4 が有効化されるが、 DefaultAppPool は v2 のままなので appcmd で v4 にする。

%windir%\system32\inetsrv/appcmd list apppool "DefaultAppPool" | find /i "v4.0"
IF "%ERRORLEVEL%"=="1" %windir%\system32\inetsrv/appcmd set apppool "DefaultAppPool" /managedRuntimeVersion:v4.0"