rirb : remote irb

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