@m_seki の

I like ruby tooから引っ越し

笑われた件を読みにくく書きます。

状況。たくさんの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

そうそう。この他にCGIインターフェイスが要る。

#!/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)