From a64913eafbee3501a677b1232470838a4ad0fc65 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 5 Jun 2009 22:16:47 -0700 Subject: Unicorn::App::Inetd: reinventing Unix, poorly :) This includes an example of tunneling the git protocol inside a TE:chunked HTTP request. The example is unfortunately contrived in that it relies on the custom examples/cat-chunk-proxy.rb script in the client. My initial wish was to have a generic tool like curl(1) operate like this: cat > ~/bin/cat-chunk-proxy.sh < e + end + s.write("0\r\n\r\n") +} + +output = fork { + $0 = "output #$0" + + c = Unicorn::ChunkedReader.new + c.reopen(s, body) + begin + loop { $stdout.write(c.readpartial(16384, buf)) } + rescue EOFError,Errno::EPIPE,Errno::EBADF,Errno::EINVAL => e + end +} + +2.times { + pid, status = Process.waitpid2 + $stderr.write("reaped: #{status.inspect}\n") unless status.success? +} diff --git a/examples/git.ru b/examples/git.ru new file mode 100644 index 0000000..3762d3d --- /dev/null +++ b/examples/git.ru @@ -0,0 +1,9 @@ +#\-E none +require 'unicorn/app/inetd' + +use Rack::Lint +use Rack::Chunked +# run Unicorn::App::Inetd.new('tee', '/tmp/tee.out') +run Unicorn::App::Inetd.new( + *%w(git daemon --verbose --inetd --export-all --base-path=/home/ew/unicorn) +) diff --git a/lib/unicorn/app/inetd.rb b/lib/unicorn/app/inetd.rb new file mode 100644 index 0000000..97dc5d3 --- /dev/null +++ b/lib/unicorn/app/inetd.rb @@ -0,0 +1,106 @@ +# this class *must* be used with Rack::Chunked + +module Unicorn::App + class Inetd + + Z = '' + Z.force_encoding(Encoding::BINARY) if Z.respond_to?(:force_encoding) + + class CatBody + def initialize(env, cmd) + @cmd = cmd + @input, @errors = env['rack.input'], env['rack.errors'] + in_rd, in_wr = IO.pipe + @err_rd, err_wr = IO.pipe + @out_rd, out_wr = IO.pipe + + @cmd_pid = fork { + inp, out, err = (0..2).map { |i| IO.new(i) } + inp.reopen(in_rd) + out.reopen(out_wr) + err.reopen(err_wr) + [ in_rd, in_wr, @err_rd, err_wr, @out_rd, out_wr ].each { |io| + io.close + } + exec(*cmd) + } + [ in_rd, err_wr, out_wr ].each { |io| io.close } + [ in_wr, @err_rd, @out_rd ].each { |io| io.binmode } + in_wr.sync = true + + # Unfortunately, input here must be processed inside a seperate + # thread/process using blocking I/O since env['rack.input'] is not + # IO.select-able and attempting to make it so would trip Rack::Lint + @inp_pid = fork { + [ @err_rd, @out_rd ].each { |io| io.close } + buf = Z.dup + + # this is dependent on @input.read having readpartial semantics: + while @input.read(16384, buf) + in_wr.write(buf) + end + in_wr.close + } + in_wr.close + end + + def each(&block) + buf = Z.dup + begin + rd, = IO.select([@err_rd, @out_rd]) + rd && rd.first or next + + if rd.include?(@err_rd) + begin + @errors.write(@err_rd.read_nonblock(16384, buf)) + rescue Errno::EINTR + rescue Errno::EAGAIN + break + end while true + end + + rd.include?(@out_rd) or next + + begin + yield @out_rd.read_nonblock(16384, buf) + rescue Errno::EINTR + rescue Errno::EAGAIN + break + end while true + rescue EOFError,Errno::EPIPE,Errno::EBADF,Errno::EINVAL + break + end while true + + self + end + + def close + @input = nil + [ [ @cmd.inspect, @cmd_pid ], [ 'input streamer', @inp_pid ] + ].each { |str, pid| + begin + pid, status = Process.waitpid2(pid) + status.success? or + @errors.write("#{str}: #{status.inspect} (PID:#{pid})\n") + rescue Errno::ECHILD + @errors.write("Failed to reap #{str} (PID:#{pid})\n") + end + } + end + + end + + def initialize(*cmd) + # enable streaming input mode in Unicorn + Unicorn::HttpRequest::DEFAULTS["unicorn.stream_input"] = true + @cmd = cmd + end + + def call(env) + [ 200, { 'Content-Type' => 'application/octet-stream' }, + CatBody.new(env, @cmd) ] + end + + end + +end -- cgit v1.2.3-24-ge0c7