恥ずかしいけど。
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)
ちなみにまだ初刷買えます。