yahns.git  about / heads / tags
sleepy, multi-threaded, non-blocking application server for Ruby
blob 6bb40c13662f93751abb745c9b03f1dd490c2972 3629 bytes (raw)
$ git show maint: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
 
# -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
# License: GPLv2 or later (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...
#
#   run ExecCgi.new('/path/to/cgit.cgi') # cgit: https://git.zx2c4.com/cgit/
#
class ExecCgi
  class MyIO < Kgio::Pipe
    attr_writer :my_pid
    attr_writer :body_tip

    def each
      buf = @body_tip || ''.dup
      if buf.size > 0
        yield buf
      end
      while tmp = kgio_read(8192, buf)
        yield tmp
      end
      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 closed?
      super
      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
  ).map(&:freeze)  # frozen strings are faster for Hash assignments

  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"
  end

  # Calls the app
  def call(env)
    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_/ }
    pipe = MyIO.pipe
    errbody = pipe[0]
    errbody.my_pid = Process.spawn(cgi_env.merge!(@env), *@args,
                                   out: pipe[1], close_others: true)
    pipe[1].close
    pipe = pipe[0]

    if head = pipe.kgio_read(8192)
      until head =~ /\r?\n\r?\n/
        tmp = pipe.kgio_read(8192) or break
        head << tmp
      end
      head, body = head.split(/\r?\n\r?\n/, 2)
      pipe.body_tip = body

      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, pipe ]
    else
      [ 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