loader.rb 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. require 'rack/utils'
  2. require 'thread'
  3. module Shotgun
  4. # Rack app that forks, loads the rackup config in the child process,
  5. # processes a single request, and exits. The response is communicated over
  6. # a unidirectional pipe.
  7. class Loader
  8. include Rack::Utils
  9. attr_reader :rackup_file
  10. def initialize(rackup_file, &block)
  11. @rackup_file = rackup_file
  12. @config = block || Proc.new { }
  13. end
  14. def call(env)
  15. dup.call!(env)
  16. end
  17. def call!(env)
  18. @env = env
  19. @reader, @writer = IO.pipe
  20. Shotgun.before_fork!
  21. if @child = fork
  22. proceed_as_parent
  23. else
  24. Shotgun.after_fork!
  25. proceed_as_child
  26. end
  27. end
  28. ##
  29. # Stuff that happens in the parent process
  30. def proceed_as_parent
  31. @writer.close
  32. rand
  33. result, status, headers = Marshal.load(@reader)
  34. body = Body.new(@child, @reader)
  35. case result
  36. when :ok
  37. [status, headers, body]
  38. when :error
  39. error, backtrace = status, headers
  40. body.close
  41. [
  42. 500,
  43. {'Content-Type'=>'text/html;charset=utf-8'},
  44. [format_error(error, backtrace)]
  45. ]
  46. else
  47. fail "unexpected response: #{result.inspect}"
  48. end
  49. end
  50. class Body < Struct.new(:pid, :fd)
  51. def each
  52. while chunk = fd.read(1024)
  53. yield chunk
  54. end
  55. end
  56. def close
  57. fd.close
  58. ensure
  59. Process.wait(pid)
  60. end
  61. end
  62. def format_error(error, backtrace)
  63. "<h1>Boot Error</h1>" +
  64. "<p>Something went wrong while loading <tt>#{escape_html(rackup_file)}</tt></p>" +
  65. "<h3>#{escape_html(error)}</h3>" +
  66. "<pre>#{escape_html(backtrace.join("\n"))}</pre>"
  67. end
  68. ##
  69. # Stuff that happens in the child process
  70. def proceed_as_child
  71. boom = false
  72. @reader.close
  73. status, headers, body = assemble_app.call(@env)
  74. Marshal.dump([:ok, status, headers.to_hash], @writer)
  75. spec_body(body).each { |chunk| @writer.write(chunk) }
  76. rescue Object => boom
  77. Marshal.dump([
  78. :error,
  79. "#{boom.class.name}: #{boom.to_s}",
  80. boom.backtrace
  81. ], @writer)
  82. ensure
  83. @writer.close
  84. exit! boom ? 1 : 0
  85. end
  86. def assemble_app
  87. config = @config
  88. inner_app = self.inner_app
  89. Rack::Builder.new {
  90. instance_eval(&config)
  91. run inner_app
  92. }.to_app
  93. end
  94. def inner_app
  95. if rackup_file =~ /\.ru$/
  96. config = File.read(rackup_file)
  97. eval "Rack::Builder.new {( #{config}\n )}.to_app", nil, rackup_file
  98. else
  99. require File.expand_path(rackup_file)
  100. if defined? Sinatra::Application
  101. Sinatra::Application.set :reload, false
  102. Sinatra::Application.set :logging, false
  103. Sinatra::Application.set :raise_errors, true
  104. Sinatra::Application
  105. else
  106. Object.const_get(camel_case(File.basename(rackup_file, '.rb')))
  107. end
  108. end
  109. end
  110. def camel_case(string)
  111. string.split("_").map { |part| part.capitalize }.join
  112. end
  113. def spec_body(body)
  114. if body.respond_to? :to_str
  115. [body]
  116. elsif body.respond_to?(:each)
  117. body
  118. else
  119. fail "body must respond to #each"
  120. end
  121. end
  122. end
  123. end