Lisp:よくある正解を読んでいて、
実行中のサーバにつないでREPLプロンプト出して関数を置き換えちゃったりした場合
というのが気になった。REPL とは read-eval-print loop のことらしいので、ターミナルで動く対話環境のことと想像した。
Ruby でも実行中のプログラムに対して irb を開けたら便利かもなーと思ったので、proof-of-concept 。ソースコードは最後に書く。
次の test.rb を例に動作を説明する。
## test.rb $i = 0 100.times do $i += 1 p $i sleep 10 end
まず、test.rb を実行する。このとき rirb.rb を併せて読み込ませる。
$ ruby -rrirb test.rb 1 2 3
実行開始直後に test.rb.rirb という UNIX ソケットのファイルが生成される。別のターミナルから、crirb.rb を実行する。
$ ./crirb test.rb.rirb irb(main):001:0> p $i 3 => nil irb(main):002:0> $i = 100 => 100
test.rb のターミナルの出力は次のようになる。
$ ruby -rrirb test.rb 1 2 3 101 102 103
印象としては、関数の定義を上書きしたりグローバル変数やインスタンス変数を観測したりするくらいならできそう。でも、ローカル変数が参照できないのは結構きついかも。というか、すでに誰かが作ってそう?
以下実装について。
rirb.rb がやることは、irb のライブラリを読み込み、UNIX ソケットを待ち受け、accept したらクライアントのソケットを入出力として irb を起動するだけ。ただし、OutputMethod を実装しても irb の出力を取れなかったので、とりあえず (ソケット接続中だけ) 標準出力を全部ソケットを流すようにした。以下ソースコード。
## rirb.rb require "irb" require "socket" module IRB # ソケットから入力する InputMethod class RemoteInputMethod < StdioInputMethod def initialize(sock) super() @sock = sock @line = [] end def gets @sock.print @prompt line = @sock.gets @line << line line end def eof? @sock.eof? end def line(line_no) @line[line_no] end end # ソケットに出力する OutputMethod # 使われている気配がない (print が呼ばれない) class RemoteOutputMethod < OutputMethod def initialize(sock) @sock = sock end def print(*opts) @sock.print(*opts) end end def IRB.rirb IRB.setup(nil) # Unix ソケット待ち受け Thread.new do UNIXServer.open($0 + ".rirb") do |serv| while $__rirb_sock = serv.accept begin # 入出力をソケットに繋ぐ im = RemoteInputMethod.new($__rirb_sock) om = RemoteOutputMethod.new($__rirb_sock) # workaround: 標準出力の write を横取りしてソケットに流す class << $stdout alias __rirb_write write def write(x) $__rirb_sock.write(x) end end # irb 起動 irb = Irb.new(nil, im, om) @CONF[:MAIN_CONTEXT] = irb.context @CONF[:PROMPT_MODE] = :DEFAULT catch(:IRB_EXIT) do irb.eval_input end rescue Exception p $! p $!.backtrace end # 標準出力の write を元に戻す class << $stdout alias write __rirb_write end end end end end end IRB.rirb
crirb はもっと単純で、Unix ソケットを開いて、標準入出力と Unix ソケットを繋ぐだけ。以下ソースコード。
#!/usr/bin/ruby ## crirb.rb require "socket" UNIXSocket.open(ARGV[0]) do |sock| loop do ios, = select([sock, $stdin]) if ios.include?($stdin) break if $stdin.eof? sock.write($stdin.readpartial(1024)) sock.flush end if ios.include?(sock) break if sock.eof? $stdout.write(sock.readpartial(1024)) $stdout.flush end end end