状況。たくさんのWindowsなPCがあって、みんなはいろんなマシンを渡り歩いてペアプロとかしてる。つまり共有のマシンなので、単一のアカウントを使って作業してるので個人の環境設定とかやりにくい。
問題。建物のレイアウトの都合でたくさんのマシンがたくさんのフロアに散ってしまった。ちょっとした連絡をとりたいと思っても面倒。メールやIRC、IPメッセンジャー、IMの類は個人の環境がないと使いにくいので、ケータイとか徒歩とかを駆使してる。
作戦。Twitterってわけじゃないけど、短いメッセージをイントラネット内にブロードキャストしよう。誰に届いたか、誰から送ったか、なんかは気にしなくて良いや。
で、どうやって?
toRubyKaigiの勉強会のコマで使ったようなシンプルなWebアプリケーションを書く。んで、新しいメッセージを誰かが書くと、同じページを開いているブラウザの画面が更新されて、ついでにタイトル(title要素)も変更する。タイトルが変わると多くのブラウザではウィンドウのタイトルバーの文字列も変えてくれるので、最小化、あるいはタイトルバーが見えるくらいの位置に移動しておけばメッセージが書き込まれたことに気付けるぞ!!
まあ、技術的には昔流行ったcometとかAjaxとかアレらと同じものでたいしたことないんですけど、タイトルバーを使って通知するっていうネタが画期的なはずだったんだよね。でもなんか笑われた。ちぇ。
実装には何年か前に書いたDiv/TofuのAjaxの仕組みを使いました。これはWebアプリ版のIPメッセンジャーを書いたときに追加した機能。
本体は187行もある超大作です。もちろんdRubyの口を持つから、いろんなイベント(例えばcheckinの情報や、Wikiの更新とか)を捕まえてみんなに通知することもできるはず。
require 'div' require 'div/js' require 'tofu/tofu' require 'tofu/tofulet' require 'rinda/tuplespace' require 'singleton' require 'rbtree' require 'digest/md5' require 'enumerator' class Njet def initialize(value) @value = value end def ===(other) @value != other end end class Store include Singleton def initialize @tree = MultiRBTree.new @ts = Rinda::TupleSpace.new(3) @ts.write([:latest, 0]) end attr_reader :tree def each_slice(n, &blk) _, key = @ts.take([:latest, nil]) @tree.each_slice(n, &blk) nil ensure @ts.write([:latest, key]) end def headline _, value = @tree.first value ? value[0] : nil end def via_drb 'drb ' + Thread.current[:DRb]['client'].stream.peeraddr[2] rescue nil end def via_cgi(context) context.webrick_req.peeraddr[2] rescue nil end def latest @ts.read([:latest, nil])[1] end def wait(key) @ts.read([:latest, Njet.new(key)], 10) rescue nil end def import_string(str) NKF.nkf('-edXm0', str) end def add(str, context=nil) str = import_string(str) from = context ? via_cgi(context) : via_drb from ||= 'local' @ts.take([:latest, nil]) begin key = -10 * Time.now.to_f @tree[key] = [str, from] str ensure @ts.write([:latest, key.to_i]) end end end class KotoSession < Div::TofuSession def initialize(bartender, hint=nil) super @content = Store.instance @base = BaseDiv.new(self) @age = nil @interval = 5000 end attr_reader :interval attr_reader :content def do_GET(context) update_div(context) context.res_header('pragma', 'no-cache') context.res_header('cache-control', 'no-cache') context.res_header('expires', 'Thu, 01 Dec 1994 16:00:00 GMT') return if do_inner_html(context) reset_age context.res_header('content-type', 'text/html; charset=euc-jp') context.res_body(@base.to_html(context)) end def wait @content.wait(@age) if @age @age = @content.latest end def reset_age @age = nil end def headline @content.headline || 'Koya' end end class BaseDiv < Div::Div set_erb('base.erb') def initialize(session) super(session) @enter = EnterDiv.new(session) @list = ListDiv.new(session) end end class EnterDiv < Div::Div set_erb('enter.erb') def do_enter(context, params) str ,= params['str'] str = '(nil)' if (str.nil? || str.empty?) @session.content.add(str, context) @session.reset_age end def div_id 'enter' end end class ListDiv < Div::Div set_erb('list.erb') 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 def initialize(session) super(session) @content = session.content @color = Color end def group_header(from, time) from + ' @ ' + time.strftime("%H:%M") end end class MyTofulet < WEBrick::CGITofulet def [](key) Store.instance if key == 'store' end end unless $DEBUG exit!(0) if fork Process.setsid exit!(0) if fork end uri = ARGV.shift || 'druby://localhost:54322' tofu = Tofu::Bartender.new(KotoSession, 'koto_' + uri.split(':').last) DRb.start_service(uri, MyTofulet.new(tofu)) unless $DEBUG STDIN.reopen('/dev/null') STDOUT.reopen('/dev/null', 'w') STDERR.reopen('/dev/null', 'w') end DRb.thread.join
- http://www.druby.org/div-1.3.3a.tar.gz - Web GUIのためのライブラリ
- http://www.druby.org/cgi-bin/cvsweb/cvsweb.cgi/div/sample/koto/ - 今回かいたネタ。erbスクリプトもここ。
#!/usr/local/bin/ruby require 'drb/drb' DRb.start_service ro = DRbObject.new_with_uri('druby://localhost:54322') ro.start(ENV.to_hash, $stdin, $stdout)