@m_seki の

I like ruby tooから引っ越し

とちぎRuby会議01のtoRuby勉強会で使ったスクリプトを説明したい。

恥ずかしいけど。

require 'rinda/tuplespace'
require 'erb'
require 'webrick/cgi'
require 'rbtree'
require 'nkf'
require 'digest/md5'
require 'enumerator'

たくさんrequireする。enumeratorはeach_sliceを使いたかったから。

class Page
  include ERB::Util

  def initialize
    @color = create_color
  end

  def create_color
    Hash.new do |h, k|
      md5 = Digest::MD5.new
      md5 << k.to_s
      r = 0b01111111 & md5.digest[0]
      g = 0b01111111 & md5.digest[1]
      b = 0b01111111 & md5.digest[2]
      h[k] = sprintf("#%02x%02x%02x", r, g, b)
    end
  end

ここからWebアプリ界隈でいうところのMVCという分類のV。ビューはテンプレートではなく、Viewオブジェクトだよ。
たとえば、送信元の情報によって見出しの色を変えたりするのはViewオブジェクトにあるのが好み。

  Base = <<EOS
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Koto</title>
<meta name="viewport" content="width=320; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;"/>
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Refresh" content="1;URL=<%= refresh_url%>"/>
<style type="text/css" media="screen">@import "../iui/iui.css";</style>
<style type="text/css">
body > ul > li {
  font-size: 14px;
}
</style>
</head>

<body>
    <div class="toolbar">
        <h1 id="pageTitle">Koto</h1>
        <a id="backButton" class="button" href="#"></a>
    </div>
    <ul id="home" title="Rinda" selected="true">
<%=h Time.now %>
<%
last_group = nil
context.each_slice(13) do |ary|
  ary.each do |k, v|
    time = Time.at(-0.1 * k)
    str, from = v
    group = group_header(from, time)
    if group != last_group %>
        <li class="group" style="font-size: 13px;color:<%= @color[from] %>"><%=h group %></li>
<%    last_group = group
    end
%>      <li><%=h str %></li><%
  end
  break
end
%>
    </ul>
</body>
</html>
EOS

たいくつなERBスクリプトですねえ。snipすべきだったか。
refreshを使ってにイージーに画面の更新をしてます。
Ajaxを使ってもよかったのだけど、どちらかというと非同期風に更新を待つ仕掛け(後述)の方がメインだったので気にしない。

これは以下の通りメソッド化されて実行時にevalしないし、ERBスクリプトに渡すパラメータはメソッドへの引数として記述できてうれしい。パラメータをムダに一般化しないように。

  ERB.new(Base).def_method(self, 'to_html(context, refresh_url)')
  def group_header(from, time)
    from + ' @ ' + time.strftime("%H:%M")
  end
end

Pageクラス終わり。

class Njet
  def initialize(value)
    @value = value
  end
  
  def ===(other)
    @value != other
  end
end

Njetは先日も書いたけど、TupleSpaceであるオブジェクトと異なるときにマッチするパターンを記述するトリック。
これを使うと、知っている状態から別の状態に遷移するまでブロックすることができる。

class Store
  def initialize
    @tree = MultiRBTree.new
    @ts = Rinda::TupleSpace.new(3)
    @ts.write([:latest, 0])
  end
  attr_reader :tree

StoreはWeb界隈のMVCでいうところのModel。よくわからんけど。
RBTreeを使って順序のある情報を管理する。
@tsは状態の変化を待ち合わせるためのTupleSpace。最新の状態は[:latest, 状態id]というタプルで表現することにした。

  def each_slice(n, &blk)
    @tree.each_slice(n, &blk)
    nil
  end

このAPIを提供したために、当日ひどい目にあった。RBTreeはあるスレッドがeach等のイテレータっぽい操作をしている間に別のスレッドが変更しようとするとTypeErrorの例外があがってしまう。
通常は問題なかったんだけど、呼び返した&blkのプロセスがロックしてしまった場合に復帰する方法がなかったのであった。とほほ。
排他制御しても本質的な解決とならないので、このインターフェイスは公開すべきでなかった。

  def via_drb
    'via drb ' + Thread.current[:DRb]['client'].stream.peeraddr[2] rescue nil
  end

dRubyでメソッドを呼び出した側の情報はスレッドの:DRbに入ってるのでした。
今日のバージョンではこういう風にすると送信元のIPアドレスを知れます。

  def via_cgi(req)
    req.peeraddr[2] rescue nil
  end

  def latest
    @ts.read([:latest, nil])[1]
  end

  def wait(key)
    @ts.read([:latest, Njet.new(key)], 5)
  end

自分が知っている状態から別の状態へ遷移したことを知るには[:latest, Njet.new(key)]でreadすればよい。最後の5はタイムアウト

  def import_string(str)
    NKF.nkf('-edXm0', str)
  end

内部はeuc-jpでした。1.8.7ばんざい

  def add(str, req=nil)
    str = import_string(str)
    from = req ? via_cgi(req) : via_drb
    from ||= 'local'
    @ts.take([:latest, nil])
    begin
      key = -10 * Time.now.to_f
      @tree[key] = [str, from]
      puts "#{Time.now.strftime('%H:%M:%S')}(#{from}) #{str}"
      str
    ensure
      @ts.write([:latest, key.to_i])
    end
  end
end

文字列を投稿するメソッド。
投稿されると、状態を表すタプルが更新される。

keyの変な計算は、並びを反転させるためと、分解能をあげるためなんだけど。そうはいってもMultiRBTreeを使ってたから分解能をあげる必要はなかったな。

class MyApp < WEBrick::CGI
  def initialize(*args)
    super(*args)
    @store = Store.new
    @page = Page.new
  end
  attr_reader :store

CGIインターフェイスであり、このアプリケーションそのものでもあるMyAppクラス。StoreとPageを抱えてる。

  def do_str(req)
    return unless req.query['str']
    str = req.query['str'].to_s
    @store.add(str, req)
  end

当日は説明し忘れたけど、http経由でも投稿できたみたい。

  def do_age(req)
    return unless req.query['age']
    age = req.query['age'].to_i
    @store.wait(age)
  rescue
  end

コメット風に待つためのしかけ。ageで指定した状態から別の状態になるまで返事を待ってくれる。

  def prepare(req)
    do_str(req)
    do_age(req)
  end
  
  def do_GET(req, res)
    res['content-type'] = 'text/html; charset= EUC-JP'
    prepare(req)
    uri = req.request_uri.dup
    uri.query = "age=#{@store.latest}"
    res.body = @page.to_html(@store, uri.to_s)
    res.status = 200
  end

  def do_POST(req, res); do_GET(req, res); end
end

class Front
  def initialize(app)
    @app = app
  end

  def [](key)
    return @app.store if key == 'store'
    @app
  end
end

このプロセスが公開するオブジェクトは二つで、一方はMyApp、もう一方はMyAppの持つStoreでした。
みんながirbなどを使って投稿してたのはStoreのほうね。

app = MyApp.new
DRb.start_service('druby://:54321', Front.new(app))
DRb.thread.join

最後にFront.new(app)を公開してDRb.thread.joinで常駐しちゃう。
なんという大作!!

かなりどうでもいいけど、CGIそのものは次の通りです。

#!/usr/local/bin/ruby
require 'drb/drb'

DRb.start_service
ro = DRbObject.new_with_uri('druby://localhost:54321?cgi')
ro.start(ENV.to_hash, $stdin, $stdout)

ちなみにまだ初刷買えます。

dRubyによる分散・Webプログラミング