yahns.git  about / heads / tags
sleepy, multi-threaded, non-blocking application server for Ruby
blob a04087d04e3b0061b651f1620e16376d0f742757 3894 bytes (raw)
$ git show HEAD:extras/exec_cgi.rb	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
 
# -*- encoding: binary -*-
# Copyright (C) 2013-2018 all contributors <yahns-public@yhbt.net>
# License: GPL-2.0+ <https://www.gnu.org/licenses/gpl-2.0.txt>
# frozen_string_literal: true
#
# if running under yahns, worker_processes is recommended to avoid conflicting
# with the SIGCHLD handler in yahns.

# Be careful if using Rack::Deflater, this needs the following commit
# (currently in rack.git, not yet in 1.5.2):
#  commit 7bda8d485b38403bf07f43793d37b66b7a8281d6
#  (delfater: ensure that parent body is always closed)
# Otherwise you will get zombies from HEAD requests which accept compressed
# responses.
#
# Usage in config.ru using cgit as an example:
#
#   use Rack::Chunked
#   # other Rack middlewares can go here...
#
#   # cgit: https://git.zx2c4.com/cgit/
#   run ExecCgi.new('/path/to/cgit.cgi', opts)
#
class ExecCgi
  class MyIO
    attr_writer :my_pid
    attr_writer :body_tip
    attr_reader :rd

    def initialize(rd)
      @rd = rd
    end

    def each
      buf = @body_tip
      yield buf unless buf.empty?
      buf = @body_tip = nil

      case tmp = @rd.read_nonblock(8192, exception: false)
      when :wait_readable
        @rd.wait_readable
      when nil
        break
      else # String
        yield tmp.freeze
      end while true
      self
    ensure
      # do this sooner, since the response body may be buffered, we want
      # to release our FD as soon as possible.
      close
    end

    def close
      # yahns will call this again after its done writing the response
      # body, so we must ensure its idempotent.
      # Note: this object (and any client-specific objects) will never
      # be shared across different threads, so we do not need extra
      # mutual exclusion here.
      return if @rd.closed?
      @rd.close
      begin
        Process.waitpid(@my_pid)
      rescue Errno::ECHILD
      end if defined?(@my_pid) && @my_pid
    end
  end

  PASS_VARS = %w(
    CONTENT_LENGTH
    CONTENT_TYPE
    AUTH_TYPE
    PATH_INFO
    PATH_TRANSLATED
    QUERY_STRING
    REMOTE_ADDR
    REMOTE_HOST
    REMOTE_IDENT
    REMOTE_USER
    REQUEST_METHOD
    SERVER_NAME
    SERVER_PORT
    SERVER_PROTOCOL
    SERVER_SOFTWARE
    SCRIPT_NAME
  )

  def initialize(*args)
    @env = Hash === args[0] ? args.shift : {}
    @args = args
    first = args[0] or
      raise ArgumentError, "need path to executable"
    first[0] == ?/ or args[0] = ::File.expand_path(first)
    File.executable?(args[0]) or
      raise ArgumentError, "#{args[0]} is not executable"
    @opts = Hash === args[-1] ? args.pop : {}
  end

  # Calls the app
  def call(env)
    env.delete('HTTP_PROXY') # ref: https://httpoxy.org/
    cgi_env = { "GATEWAY_INTERFACE" => "CGI/1.1" }
    PASS_VARS.each { |key| val = env[key] and cgi_env[key] = val }
    env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }

    rd, wr = IO.pipe
    io = MyIO.new(rd)
    errbody = io
    errbody.my_pid = spawn(cgi_env.merge!(@env), *@args,
                           @opts.merge(out: wr, close_others: true))
    wr.close

    begin
      head = rd.readpartial(8192)
      until head =~ /\r?\n\r?\n/
        tmp = rd.readpartial(8192)
        head << tmp
        tmp.clear
      end
      head, body = head.split(/\r?\n\r?\n/, 2)
      io.body_tip = body.freeze

      env["HTTP_VERSION"] ||= "HTTP/1.0" # stop Rack::Chunked for HTTP/0.9

      headers = Rack::Utils::HeaderHash.new
      prev = nil
      head.split(/\r?\n/).each do |line|
        case line
        when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
        when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
        end
      end
      status = headers.delete("Status") || 200
      errbody = nil
      [ status, headers, io ]
    rescue EOFError
      [ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
    end
  ensure
    errbody.close if errbody
  end
end

git clone git://yhbt.net/yahns.git
git clone https://yhbt.net/yahns.git