2009/03/14

Ruby で UPnP サービスを作る

UPnPは主にホームネットワークなど、管理者のいないネットワークで使われることを想定としたプロトコルです。

UPnP を用いることで ad hoc にサービスを提供したり利用したりすることが可能になります。このプトロコルをベースにしたサービスとしては、例えば Windows の ICS 機能(インターネット接続共有機能)のポートマッピングやDLNAなどが有名です。

規格策定から10年ほどが過ぎ、SOAPベースのプロトコルなど、やや古めかしさを感じる点もありますが、サービスの自動発見など、家庭内ネットワークで気軽にサービスを提供するには便利な仕様です。

というわけで、Rubyを使って簡易UPnPデバイス(サービス提供側)とUPnPコントロールポイント(サービス利用側)を作ってみましょう。

UPnPになじみのない方は、UPnP Device Architectureに目を通しておくことをお勧めします(以下のコードは、UPnp DA 1.0に準拠しています)。

今回は簡単に、'/'ファイルシステムがあるディスク容量を得るためのサービスを書いてみます。デバイスを SystemStatServer、そのデバイスが提供するサービスを DiskStat としました。

ここで解説している全コードは、githubから取得できますので、必要に応じてご利用ください。
  % git clone git://github.com/n-miyo/ru-sss.git

Ruby には、非常に優れた UPnP ライブラリが存在します。作者は rdoc のメインテナンスを担当されている Eric Hodel さんで、現在のバージョンは1.1.0です。

例えば UPnP では、サービス提供側のデバイスが SCPD と呼ばれるSOAP アクション一覧を記した XML ファイルを公開することが必須になっています。多くの UPnP 実装では、このXMLは活用していないのですが、Ruby UPnP 実装では、デバイスから SCPD ファイルを取得、解釈し、SOAP アクションを呼び出すメソッドを動的に作成してくれます。このため、クライアントは、該当メソッドを使うコードを書くことに集中できます。

また、サービス提供側でも、提供したいアクションを登録しておくだけでリフレクションにより SCPD XMLファイルを自動的に生成してくれるなど、Ruby の良さを効果的に生かした、大変美しい実装になっ
ています。

まずはこの UPnP ライブラリをインストールします。

幸い gem で公開されているので本来は、gem install で利用準備は終了です。
  % sudo gem install UPnP

ただ、私の環境ではいくつか期待通りに動作しないバグがあり、修正が必要でした。既に patch はtrackerへ登録済みですが、サンプルコードと一緒に公開もしていますので、必要に応じて利用してください(以下のサンプルコードは、このパッチが適用されていることを前提にしています)。

最初にサービス提供側のコードを書いてみます。

基本的には、UPnP::Deviceを継承してデバイスの定義を行い、そのデバイスが提供するサービスをUPnP::Service を継承して書くことになります。

デバイスの基本は、以下の通りです。
  1. 提供したいサービスに ServiceID を付与しておく
  2. UPnP::Device.create を使ってデバイスを作成する
  3. そのデバイスに対し、manufacture などの基本情報を付与する
  4. そのデバイスが提供するServiceIDを登録する
lib/UPnP/device/system_stat_server.rb の self.run がこのコードです。
def self.run(argv = ARGV)
super

device = create 'SystemStatServer', @options[:name] do |s|
s.manufacturer = 'Tempus.ORG'
s.manufacturer_url = 'http://www.tempus.org'

s.model_description = "Disk Stat version #{s.class::VERSION}"
s.model_name = 'Disk Stat'
s.model_url = 'http://www.tempus.org/'
s.model_number = UPnP::Device::SystemStatServer::VERSION

s.add_service 'DiskStat'
end

device.run
end

次に提供するサービスを実装しましょう。ディスク容量を取得するには、RubyForge で公開されている sys-filesystem を使いましょう。これも gem でインストールするだけです。
  % sudo gem install sys-filesystem

BSDや MacOS X を使っている人はバージョンに注意してください。

サービスの定義では、サービス名称とIN/OUTの種別、ならびに結びつけるstate変数を指定します。ここでは、アクション名称をGetDiskFreeSpace とし、取得したパスと単位を指定できるようにしてみました。lib/UPnP/service/disk_stat.rb が該当のコードです。
add_action 'GetDiskFreeSpace',
[IN, 'FileSystem', 'A_ARG_TYPE_FileSystem'],
[IN, 'Unit', 'A_ARG_TYPE_Unit'],

[OUT, 'FreeSpace', 'A_ARG_TYPE_FreeSpace']

add_variable では、UPnP 上でのstate変数と型とを結びつけています。今回は GENA イベントを利用しないので、非常にシンプルな定義です。
add_variable 'A_ARG_TYPE_FileSystem', 'string'
add_variable 'A_ARG_TYPE_Unit', 'string'
add_variable 'A_ARG_TYPE_FreeSpace', 'i4'

あとは該当サービスの内容を実装するだけです。戻り値の第一要素には RETVAL を指定しますが、今回のサービスでは利用しないので nil を指定しています。
def GetDiskFreeSpace(file_system, unit)
result = 0

begin
stat = Sys::Filesystem.stat(file_system)

s = stat.blocks_available * stat.fragment_size
case unit
when 'G', 'g'
result = s / (1024 * 1024 * 1024)
when 'M', 'm'
result = s / (1024 * 1024)
when 'K', 'k'
result = s / 1024
else
result = s
end
rescue
result = 0
end

[nil, result]
end

最後にデバイスの実行コードを定義しましょう。基本はこれだけです。bin/upnp_system_stat_server が該当コードです。
UPnP::Device::SystemStatServer.run

次に呼び出し側であるコントロールポイントの実装です。

こちらもデバイスと同様、UPnP::Control::Device とUPnP::Control::Service を継承して作成します。

まずはサービスから実装します。と言っても、この1行で完成です。
URN_1 = [UPnP::SERVICE_SCHEMA_PREFIX, name.split(':').last, 1].join ':'

次に利用したいサービスのインスタンスを取得するコードを書きます。今回のデバイスでは単一サービスしか提供していませんが、複数のサービスを提供するデバイスもありますから、適切なサービスを選択するようにしましょう。
def disk_stat
@ds ||= services.find do |service|
service.type == UPnP::Control::Service::DiskStat::URN_1
end

@ds
end

alias ds disk_stat

これでサービスを利用する準備が整いました。

最も大事な、このサービスの GetDiskFreeSpace アクションを利用するコードを書きます。単純に該当アクション名を method として実行するだけでサービスを利用できます。
def print_free_size(path = '/', unit = 'm')
puts "server: #{friendly_name || presentation_url}"
size = ds.GetDiskFreeSpace path, unit
puts "free size: #{size} #{unit.upcase} bytes."
end

最後に、デバイスの VERSION と URN を定義します。

VERSION = '1.0.0'

URN_1 = [UPnP::DEVICE_SCHEMA_PREFIX, name.split(':').last, 1].join ':'

基本的に、サービス利用側が書く必要のあるコードはこれだけです。その他必要なコードはすべてライブラリが必要に応じて動的に生成してくれます。

早速作ったコードを試してみましょう。

FreeBSD と Ubuntu の上ででデバイスを稼働させます。
  freebsd% RUBYLIB=lib bin/upnp_system_stat_server -n freebsd
ubuntu% RUBYLIB=lib bin/upnp_system_stat_server -n ubuntu

デバイスが立ち上がったのを確認して、MacOS X 上で コントロールポイントを実行します。
  macosx% ruby -I ../../UPnP-1.1.0/lib -I../lib upnp_sss
server: ubuntu
free size: 25698 M bytes.
server: freebsd
free size: 134 M bytes.
macosx%

無事、動的に機器発見を行い、サービスを利用して情報を取得することができました。

この実装で書いたコード行は、デバイスで100行、コントロールポイントにいたっては、安心を見越したコードでありながらわずか48行です。Rubyの強力さとUPnPライブラリの洗練さとを十分に感じることが出来ます。

なお、このライブラリではデバイス側とコントロールポイントを同じホストマシン上で動作させることができないようになっています。同一ホストマシン上でデバイスとコントロールポイントを動かしたい場合には、サンプルコードに同梱してあるパッチを試してみてください。

是非、Ruby UPnPライブラリを用いて、ホームネットワークでのサービスを試してみてはいかがでしょうか?

大変有益なライブラリを公開してくださり、また、サンプルコードの参考となった IGD や MediaServer を公開してくださったEric さんに、改めて感謝いたします。

0 件のコメント:

コメントを投稿