# patch core classes first require_relative "patches/mini_support" # master require require "fiber" require "cgi" require "uri" require "openssl" require "socket" require "tilt" require "time" require_relative "../../ext/nyara" require_relative "hashes/param_hash" require_relative "hashes/header_hash" require_relative "hashes/config_hash" require_relative "mime_types" require_relative "controller" require_relative "request" require_relative "cookie" require_relative "session" require_relative "flash" require_relative "config" require_relative "route" require_relative "route_entry" require_relative "view" require_relative "cpu_counter" require_relative "part" # default controllers require_relative "controllers/public_controller" module Nyara HTTP_STATUS_FIRST_LINES = Hash[HTTP_STATUS_CODES.map{|k,v|[k, "HTTP/1.1 #{k} #{v}\r\n".freeze]}].freeze HTTP_REDIRECT_STATUS = [300, 301, 302, 303, 307] # base header response for 200 # caveat: these entries can not be deleted OK_RESP_HEADER = HeaderHash.new OK_RESP_HEADER['Content-Type'] = 'text/html; charset=UTF-8' OK_RESP_HEADER['Cache-Control'] = 'no-cache' OK_RESP_HEADER['Transfer-Encoding'] = 'chunked' OK_RESP_HEADER['X-XSS-Protection'] = '1; mode=block' OK_RESP_HEADER['X-Content-Type-Options'] = 'nosniff' OK_RESP_HEADER['X-Frame-Options'] = 'SAMEORIGIN' START_CTX = { 0 => $0.dup, argv: ARGV.map(&:dup), cwd: (begin a = File.stat(pwd = ENV['PWD']) b = File.stat(Dir.pwd) a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd rescue Dir.pwd end) } class << self def config raise ArgumentError, 'block not accepted, did you mean Nyara::Config.config?' if block_given? Config end def setup Session.init Config.init Route.compile # todo lint if SomeController#request is re-defined View.init end def start_server port = Config[:port] puts "starting #{Config[:env]} server at 0.0.0.0:#{port}" case Config[:env].to_s when 'production' patch_tcp_socket start_production_server port when 'test' # don't else patch_tcp_socket start_development_server port end end def patch_tcp_socket puts "patching TCPSocket" require_relative "patches/tcp_socket" end def start_development_server port trap :INT do exit! end t = Thread.new do server = TCPServer.new '0.0.0.0', port server.listen 1000 Ext.init_queue Ext.run_queue server.fileno end t.join end # Signals: # # [INT] kill -9 all workers, and exit # [QUIT] graceful quit all workers, and exit if all children terminated # [TERM] same as QUIT # [USR1] restore worker number # [USR2] graceful spawn a new master and workers, with all content respawned # [TTIN] increase worker number # [TTOUT] decrease worker number # # To make a graceful hot-restart: # # 1. USR2 -> old master # 2. if good (workers are up, etc), QUIT -> old master, else QUIT -> new master and fail # 3. if good (requests are working, etc), INT -> old master # else QUIT -> new master and USR1 -> old master to restore workers # # NOTE in step 2/3 if an additional fork executed in new master and hangs, # you may need send an additional INT to terminate it.
# NOTE hot-restart reloads almost everything, including Gemfile changes and configures except port. # but, if some critical environment variable or port configure needs change, you still need cold-restart.
# TODO write to a file to show workers are good
# TODO detect port config change def start_production_server port workers = Config[:workers] puts "workers: #{workers}" if (server_fd = ENV['NYARA_FD'].to_i) > 0 puts "inheriting server fd #{server_fd}" @server = TCPServer.for_fd server_fd end unless @server @server = TCPServer.new '0.0.0.0', port @server.listen 1000 ENV['NYARA_FD'] = @server.fileno.to_s end GC.start @workers = [] workers.times do incr_workers nil end trap :INT, &method(:kill_all) trap :QUIT, &method(:quit_all) trap :TERM, &method(:quit_all) trap :USR2, &method(:spawn_new_master) trap :USR1, &method(:restore_workers) trap :TTIN do if Config[:workers] > 1 Config[:workers] -= 1 decr_workers nil end end trap :TTOU do Config[:workers] += 1 incr_workers nil end Process.waitall end private # kill all workers and exit def kill_all sig @workers.each do |w| Process.kill :KILL, w end exit! end # graceful quit all workers and exit def quit_all sig until @workers.empty? decr_workers sig end # wait will finish the wait-and-quit job end # spawn a new master def spawn_new_master sig fork do @server.close_on_exec = false Dir.chdir START_CTX[:cwd] if File.executable?(START_CTX[0]) exec START_CTX[0], *START_CTX[:argv], close_others: false else # gemset env should be correct because env is inherited require "rbconfig" ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) exec ruby, START_CTX[0], *START_CTX[:argv], close_others: false end end end # restore number of workers as Config def restore_workers sig (Config[:workers] - @workers.size).times do incr_workers sig end end # graceful decrease worker number by 1 def decr_workers sig w = @workers.shift puts "killing worker #{w}" Process.kill :QUIT, w end # increase worker number by 1 def incr_workers sig pid = fork { $0 = "(nyara:worker) ruby #{$0}" trap :QUIT do Ext.graceful_quit @server.fileno end trap :TERM do Ext.graceful_quit @server.fileno end t = Thread.new do Ext.init_queue Ext.run_queue @server.fileno end t.join } @workers << pid end end end