From 182c836869426f2a236b2bb73288568e69557584 Mon Sep 17 00:00:00 2001 From: schneems Date: Thu, 14 Jan 2016 09:41:55 -0600 Subject: [PATCH] Initial Seperation of CLI and Server Launcher work This is a WIP. This was the minimum I could do to get all tests to pass without changing any tests. Eventually I think we want all high level process controls to come from launcher, I also think we want another separate object that gets passed to Runner/Single/Cluster that will maintain a relationship with the Launcher. We could use this as the object that also gets exposed to the app like the Embeddable class we talked about earlier. Moving forwards i'm planning to port out the CLI tests to only test that they are parsing the correct config and launching servers. I'll port all low level unit tests over to the launcher. Making this change we could either keep all the public methods in CLI that delegate to `@launcher`, I'm guessing not many people are using the internals of CLI and we can take them out. It's your call though. Wanted to kick this over the fence and see if you had any strong reactions or feelings about this approach. --- lib/puma.rb | 1 + lib/puma/cli.rb | 242 ++++++++++-------------------------- lib/puma/launcher.rb | 288 +++++++++++++++++++++++++++++++++++++++++++ test/test_cli.rb | 2 + 4 files changed, 354 insertions(+), 179 deletions(-) create mode 100644 lib/puma/launcher.rb diff --git a/lib/puma.rb b/lib/puma.rb index 49792e14..678415c1 100644 --- a/lib/puma.rb +++ b/lib/puma.rb @@ -12,3 +12,4 @@ require 'thread' # Ruby Puma require 'puma/const' require 'puma/server' +require 'puma/launcher' diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index ce8b25a4..8e5b7bda 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -12,6 +12,7 @@ require 'puma/single' require 'puma/cluster' require 'puma/commonlogger' +require 'puma/launcher' module Puma class << self @@ -53,8 +54,61 @@ module Puma @binder = Binder.new(@events) @binder.import_from_env + + begin + @parser.parse! @argv + @cli_options[:rackup] = @argv.shift if @argv.last + rescue UnsupportedOption + exit 1 + end + + @launcher = Puma::Launcher.new(@cli_options) + + @launcher.events = self.events + @launcher.config = self.config + @launcher.binder = self.binder + @launcher.setup(@options) + end + ## BACKWARDS COMPAT FOR TESTS + + def delete_pidfile + @launcher.delete_pidfile + end + + def log(string) + @launcher.log(string) + end + + def stop + @launcher.stop + end + + def restart + @launcher.restart + end + + def write_state + @launcher.write_state + end + + def write_pid + @launcher.write_pid + end + + private + def parse_options + @launcher.send(:parse_options) + end + + def set_rack_environment + @launcher.send(:set_rack_environment) + end + public + + ## BACKWARDS COMPAT FOR TESTS + # The Binder object containing the sockets bound to. attr_reader :binder @@ -67,12 +121,6 @@ module Puma # The Events object used to output information. attr_reader :events - # Delegate +log+ to +@events+ - # - def log(str) - @events.log str - end - # Delegate +error+ to +@events+ # def error(str) @@ -84,21 +132,21 @@ module Puma end def clustered? + # remove eventually @options[:workers] > 0 end - def prune_bundler? - @options[:prune_bundler] && clustered? && !@options[:preload_app] - end - def jruby? + # remove eventually IS_JRUBY end def windows? + # remove eventually RUBY_PLATFORM =~ /mswin32|ming32/ end +<<<<<<< HEAD def env @options[:environment] || @cli_options[:environment] || ENV['RACK_ENV'] || 'development' end @@ -144,6 +192,8 @@ module Puma log "- Goodbye!" end +======= +>>>>>>> Initial Seperation of CLI and Server Launcher work def jruby_daemon_start require 'puma/jruby_restart' JRubyRestart.daemon_start(@restart_dir, restart_args) @@ -184,57 +234,8 @@ module Puma # for it to finish. # def run - begin - parse_options - rescue UnsupportedOption - exit 1 - end - - dir = @options[:directory] - Dir.chdir(dir) if dir - - prune_bundler if prune_bundler? - - set_rack_environment - - if clustered? - @events.formatter = Events::PidFormatter.new - @options[:logger] = @events - - @runner = Cluster.new(self) - else - @runner = Single.new(self) - end - - setup_signals - set_process_title - - @status = :run - - @runner.run - - case @status - when :halt - log "* Stopping immediately!" - when :run, :stop - graceful_stop - when :restart - log "* Restarting..." - @runner.before_restart - restart! - when :exit - # nothing - end - end - - def stop - @status = :stop - @runner.stop - end - - def restart - @status = :restart - @runner.restart + @runner = @launcher.runner + @launcher.run end def reload_worker_directory @@ -254,7 +255,7 @@ module Puma end def stats - @runner.stats + @launcher.stats end def halt @@ -263,12 +264,6 @@ module Puma end private - def title - buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})" - buffer << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty? - buffer - end - def unsupported(str) @events.error(str) raise UnsupportedOption @@ -283,18 +278,6 @@ module Puma end end - def set_process_title - Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title - end - - def find_config - if @cli_options[:config_file] == '-' - @cli_options[:config_file] = nil - else - @cli_options[:config_file] ||= %W(config/puma/#{env}.rb config/puma.rb).find { |f| File.exist?(f) } - end - end - # Build the OptionParser object to handle the available options. # @@ -471,53 +454,6 @@ module Puma end end - def set_rack_environment - @options[:environment] = env - ENV['RACK_ENV'] = env - end - - def setup_signals - begin - Signal.trap "SIGUSR2" do - restart - end - rescue Exception - log "*** SIGUSR2 not implemented, signal based restart unavailable!" - end - - begin - Signal.trap "SIGUSR1" do - phased_restart - end - rescue Exception - log "*** SIGUSR1 not implemented, signal based restart unavailable!" - end - - begin - Signal.trap "SIGTERM" do - stop - end - rescue Exception - log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!" - end - - begin - Signal.trap "SIGHUP" do - redirect_io - end - rescue Exception - log "*** SIGHUP not implemented, signal based logs reopening unavailable!" - end - - if jruby? - Signal.trap("INT") do - @status = :exit - graceful_stop - exit - end - end - end - def close_binder_listeners @binder.listeners.each do |l, io| io.close @@ -526,57 +462,5 @@ module Puma File.unlink("#{uri.host}#{uri.path}") end end - - def parse_options - @parser.parse! @argv - - @cli_options[:rackup] = @argv.shift if @argv.last - - find_config - - @config = Puma::Configuration.new @cli_options - - # Advertise the Configuration - Puma.cli_config = @config - - @config.load - - @options = @config.options - - if clustered? && (jruby? || windows?) - unsupported 'worker mode not supported on JRuby or Windows' - end - - if @options[:daemon] && windows? - unsupported 'daemon mode not supported on Windows' - end - end - - def prune_bundler - return unless defined?(Bundler) - puma = Bundler.rubygems.loaded_specs("puma") - dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) } - puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') } - - unless puma_lib_dir - log "! Unable to prune Bundler environment, continuing" - return - end - - deps = puma.runtime_dependencies.map do |d| - spec = Bundler.rubygems.loaded_specs(d.name) - "#{d.name}:#{spec.version.to_s}" - end - - log '* Pruning Bundler environment' - home = ENV['GEM_HOME'] - Bundler.with_clean_env do - ENV['GEM_HOME'] = home - ENV['PUMA_BUNDLER_PRUNED'] = '1' - wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild")) - args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv - Kernel.exec(*args) - end - end end end diff --git a/lib/puma/launcher.rb b/lib/puma/launcher.rb new file mode 100644 index 00000000..10584104 --- /dev/null +++ b/lib/puma/launcher.rb @@ -0,0 +1,288 @@ +module Puma + class Launcher + def initialize(cli_options = {}) + @cli_options = cli_options + @runner = nil + end + + ## THIS STUFF IS NEEDED FOR RUNNER + + # Delegate +log+ to +@events+ + # + def log(str) + @events.log str + end + + def config + @config + end + + def stats + @runner.stats + end + + def halt + @status = :halt + @runner.halt + end + + def binder + @binder + end + + def events + @events + end + + def write_state + write_pid + + path = @options[:state] + return unless path + + state = { 'pid' => Process.pid } + cfg = @config.dup + + [ + :logger, + :before_worker_shutdown, :before_worker_boot, :before_worker_fork, + :after_worker_boot, + :on_restart, :lowlevel_error_handler + ].each { |k| cfg.options.delete(k) } + state['config'] = cfg + + require 'yaml' + File.open(path, 'w') { |f| f.write state.to_yaml } + end + + def delete_pidfile + path = @options[:pidfile] + File.unlink(path) if path && File.exist?(path) + end + + # If configured, write the pid of the current process out + # to a file. + # + def write_pid + path = @options[:pidfile] + return unless path + + File.open(path, 'w') { |f| f.puts Process.pid } + cur = Process.pid + at_exit do + delete_pidfile if cur == Process.pid + end + end + + attr_accessor :options, :binder, :config, :events + ## THIS STUFF IS NEEDED FOR RUNNER + + def setup(options) + @options = options + parse_options + + dir = @options[:directory] + Dir.chdir(dir) if dir + + prune_bundler if prune_bundler? + + set_rack_environment + + if clustered? + @events.formatter = Events::PidFormatter.new + @options[:logger] = @events + + @runner = Cluster.new(self) + else + @runner = Single.new(self) + end + + @status = :run + end + + + + attr_accessor :runner + + def stop + @status = :stop + @runner.stop + end + + def restart + @status = :restart + @runner.restart + end + + def run + setup_signals + set_process_title + @runner.run + + case @status + when :halt + log "* Stopping immediately!" + when :run, :stop + graceful_stop + when :restart + log "* Restarting..." + @runner.before_restart + restart! + when :exit + # nothing + end + end + + + def clustered? + @options[:workers] > 0 + end + + def jruby? + # remove eventually + IS_JRUBY + end + + def windows? + # remove eventually + RUBY_PLATFORM =~ /mswin32|ming32/ + end + + + def prune_bundler + return unless defined?(Bundler) + puma = Bundler.rubygems.loaded_specs("puma") + dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) } + puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') } + + unless puma_lib_dir + log "! Unable to prune Bundler environment, continuing" + return + end + + deps = puma.runtime_dependencies.map do |d| + spec = Bundler.rubygems.loaded_specs(d.name) + "#{d.name}:#{spec.version.to_s}" + end + + log '* Pruning Bundler environment' + home = ENV['GEM_HOME'] + Bundler.with_clean_env do + ENV['GEM_HOME'] = home + ENV['PUMA_BUNDLER_PRUNED'] = '1' + wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild")) + args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv + Kernel.exec(*args) + end + end + + private + def unsupported(str) + @events.error(str) + raise UnsupportedOption + end + + def parse_options + find_config + + @config = Puma::Configuration.new @cli_options + + # Advertise the Configuration + Puma.cli_config = @config + + @config.load + + @options = @config.options + + if clustered? && (jruby? || windows?) + unsupported 'worker mode not supported on JRuby or Windows' + end + + if @options[:daemon] && windows? + unsupported 'daemon mode not supported on Windows' + end + end + + def find_config + if @cli_options[:config_file] == '-' + @cli_options[:config_file] = nil + else + @cli_options[:config_file] ||= %W(config/puma/#{env}.rb config/puma.rb).find { |f| File.exist?(f) } + end + end + + def graceful_stop + @runner.stop_blocked + log "=== puma shutdown: #{Time.now} ===" + log "- Goodbye!" + end + + def set_process_title + Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title + end + + def title + buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})" + buffer << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty? + buffer + end + + def set_rack_environment + @options[:environment] = env + ENV['RACK_ENV'] = env + end + + def env + @options[:environment] || + @cli_options[:environment] || + ENV['RACK_ENV'] || + 'development' + end + + def prune_bundler? + @options[:prune_bundler] && clustered? && !@options[:preload_app] + end + + def setup_signals + begin + Signal.trap "SIGUSR2" do + restart + end + rescue Exception + log "*** SIGUSR2 not implemented, signal based restart unavailable!" + end + + begin + Signal.trap "SIGUSR1" do + phased_restart + end + rescue Exception + log "*** SIGUSR1 not implemented, signal based restart unavailable!" + end + + begin + Signal.trap "SIGTERM" do + stop + end + rescue Exception + log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!" + end + + begin + Signal.trap "SIGHUP" do + redirect_io + end + rescue Exception + log "*** SIGHUP not implemented, signal based logs reopening unavailable!" + end + + if jruby? + Signal.trap("INT") do + @status = :exit + graceful_stop + exit + end + end + end + end +end \ No newline at end of file diff --git a/test/test_cli.rb b/test/test_cli.rb index c9e4e5f3..47cbbfe7 100644 --- a/test/test_cli.rb +++ b/test/test_cli.rb @@ -82,6 +82,7 @@ class TestCLI < Test::Unit::TestCase cli.send(:parse_options) t = Thread.new { cli.run } + t.abort_on_exception = true wait_booted @@ -105,6 +106,7 @@ class TestCLI < Test::Unit::TestCase cli.send(:parse_options) t = Thread.new { cli.run } + t.abort_on_exception = true wait_booted