diff --git a/CHANGES.md b/CHANGES.md index 1ddc586..e6f66cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,15 @@ # Changes +## master + +* Add `:pumactl` option +* Add `:restart_timeout` option +* Don't stop Puma if it was not started by Guard +* Don't notify about start when no start +* Improve `guard-compat` using (https://github.com/guard/guard-compat#migrating-your-api-calls) +* Remove unused `pry` dependency +* Update versions of dependencies + ## 0.4.1 * Improve notifications. Via #30 diff --git a/README.md b/README.md index 3a384b8..d0a543c 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,21 @@ end * `:port` is the port number to run on (default `4000`) * `:environment` is the environment to use (default `development`) -* `:start_on_start` will start the server when starting Guard (default `true`) +* `:start_on_start` will start the server when starting Guard and stop the server when reloading/stopping Guard (default `true`) * `:force_run` kills any process that's holding open the listen port before attempting to (re)start Puma (default `false`). * `:daemon` runs the server as a daemon, without any output to the terminal that ran `guard` (default `false`). * `:quiet` runs the server in quiet mode, suppressing output (default `true`). * `:debugger` runs the server with the debugger enabled (default `false`). Required ruby-debug gem. * `:timeout` waits this number of seconds when restarting the Puma server before reporting there's a problem (default `20`). +* `:restart_timeout` waits this number of seconds before the next restarting the Puma server (default `1`). * `:config` is the path to the Puma config file (optional) * `:bind` is URI to bind to (tcp:// and unix:// only) (optional) * `:control_token` is the token to use as authentication for the control server(optional) * `:control_port` is the port to use for the control server(optional) * `:threads` is the min:max number of threads to use. Defaults to 0:16 (optional) +* `:pumactl` manages the server via `pumactl` executable instead of `puma` (default `false`) + * Incompatible with options such as `port`, `environment`, `daemon`, `bind`, `threads` + * Use with `config` option is preferred. * `:notifications` is the list of notification types that will be sent. Defaults to `[:restarting, :restarted, :not_restarted, :stopped]` (optional) ## Contributing diff --git a/guard-puma.gemspec b/guard-puma.gemspec index 508d6f1..8dab26a 100644 --- a/guard-puma.gemspec +++ b/guard-puma.gemspec @@ -16,8 +16,7 @@ Gem::Specification.new do |gem| gem.add_dependency "guard", "~> 2.14" gem.add_dependency "guard-compat", "~> 1.2" gem.add_dependency "puma", "~> 3.6" - gem.add_development_dependency "rake", "~> 10.4" - gem.add_development_dependency "rspec", "~> 3.5.0" - gem.add_development_dependency "guard-rspec", "~> 4.7.0" - gem.add_development_dependency "pry" + gem.add_development_dependency "rake", "~> 12" + gem.add_development_dependency "rspec", "~> 3.7" + gem.add_development_dependency "guard-rspec", "~> 4.7" end diff --git a/lib/guard/puma.rb b/lib/guard/puma.rb index 57ee237..d77ef3b 100644 --- a/lib/guard/puma.rb +++ b/lib/guard/puma.rb @@ -1,9 +1,6 @@ -require "guard" -require "guard/plugin" -require "guard/puma/runner" -require "rbconfig" -require "guard/puma/version" -require "guard/compat/plugin" +require 'guard/compat/plugin' +require_relative 'puma/runner' +require_relative 'puma/version' module Guard class Puma < Plugin @@ -14,11 +11,13 @@ def self.default_env end DEFAULT_OPTIONS = { + :pumactl => false, :port => 4000, :environment => default_env, :start_on_start => true, :force_run => false, :timeout => 20, + :restart_timeout => 1, :debugger => false, :notifications => %i[restarting restarted not_restarted stopped] } @@ -28,35 +27,54 @@ def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) @options[:port] = nil if @options.key?(:config) @runner = ::Guard::PumaRunner.new(@options) + @last_restarted = Time.now end def start + return unless options[:start_on_start] server = options[:server] ? "#{options[:server]} and " : "" - UI.info "Puma starting#{port_text} in #{server}#{options[:environment]} environment." - runner.start if options[:start_on_start] + Compat::UI.info( + "Puma starting#{port_text} in #{server}#{options[:environment]} environment." + ) + runner.start end def reload - UI.info "Restarting Puma..." + return if (Time.now - @last_restarted) < options[:restart_timeout] + @last_restarted = Time.now + Compat::UI.info "Restarting Puma..." if options[:notifications].include?(:restarting) - Notifier.notify("Puma restarting#{port_text} in #{options[:environment]} environment...", :title => "Restarting Puma...", :image => :pending) + Compat::UI.notify( + "Puma restarting#{port_text} in #{options[:environment]} environment...", + title: "Restarting Puma...", image: :pending + ) end if runner.restart - UI.info "Puma restarted" + Compat::UI.info "Puma restarted" if options[:notifications].include?(:restarted) - Notifier.notify("Puma restarted#{port_text}.", :title => "Puma restarted!", :image => :success) + Compat::UI.notify( + "Puma restarted#{port_text}.", + title: "Puma restarted!", image: :success + ) end else - UI.info "Puma NOT restarted, check your log files." + Compat::UI.info "Puma NOT restarted, check your log files." if options[:notifications].include?(:not_restarted) - Notifier.notify("Puma NOT restarted, check your log files.", :title => "Puma NOT restarted!", :image => :failed) + Compat::UI.notify( + "Puma NOT restarted, check your log files.", + title: "Puma NOT restarted!", image: :failed + ) end end end def stop + return unless options[:start_on_start] if options[:notifications].include?(:stopped) - Notifier.notify("Until next time...", :title => "Puma shutting down.", :image => :pending) + Compat::UI.notify( + "Until next time...", + title: "Puma shutting down.", image: :pending + ) end runner.halt end diff --git a/lib/guard/puma/runner.rb b/lib/guard/puma/runner.rb index 7e96f24..4b50a81 100644 --- a/lib/guard/puma/runner.rb +++ b/lib/guard/puma/runner.rb @@ -6,49 +6,48 @@ class PumaRunner MAX_WAIT_COUNT = 20 - attr_reader :options, :control_url, :control_token, :cmd_opts + attr_reader :options, :control_url, :control_token, :cmd_opts, :pumactl def initialize(options) @control_token = options.delete(:control_token) { |_| ::Puma::Configuration.random_token } - @control = "localhost" @control_port = (options.delete(:control_port) || '9293') - @control_url = "#{@control}:#{@control_port}" + @control_url = "localhost:#{@control_port}" @quiet = options.delete(:quiet) { true } + @pumactl = options.delete(:pumactl) { false } @options = options - puma_options = if options[:config] - { - '--config' => options[:config], - '--control-token' => @control_token, - '--control' => "tcp://#{@control_url}", - '--environment' => options[:environment] - } + puma_options = { + puma_options_key(:config) => options[:config], + puma_options_key(:control_token) => @control_token, + puma_options_key(:control_url) => "tcp://#{@control_url}" + } + if options[:config] + puma_options['--config'] = options[:config] else - { - '--port' => options[:port], - '--control-token' => @control_token, - '--control' => "tcp://#{@control_url}", - '--environment' => options[:environment] - } - end - [:bind, :threads].each do |opt| - puma_options["--#{opt}"] = options[opt] if options[opt] + puma_options['--port'] = options[:port] end + %i[bind threads environment] + .select { |opt| options[opt] } + .each do |opt| + if pumactl + Compat::UI.warning( + "`#{opt}` option is not compatible with `pumactl` option" + ) + else + puma_options["--#{opt}"] = options[opt] + end + end puma_options = puma_options.to_a.flatten - puma_options << '-q' if @quiet + puma_options << '--quiet' if @quiet @cmd_opts = puma_options.join ' ' end def start - if in_windows_cmd? - Kernel.system windows_start_cmd - else - Kernel.system nix_start_cmd - end + Kernel.system build_command('start') end def halt - Net::HTTP.get build_uri('halt') + run_puma_command!('halt') # server may not have been stopped correctly, but we are halting so who cares. return true end @@ -68,8 +67,30 @@ def sleep_time private + PUMA_OPTIONS_KEYS_BY_PUMACTL = { + true => { + config: '--config-file', + control_url: '--control-url' + }.freeze, + false => { + config: '--config', + control_url: '--control' + }.freeze + }.freeze + + private_constant :PUMA_OPTIONS_KEYS_BY_PUMACTL + + def puma_options_key(key) + keys = PUMA_OPTIONS_KEYS_BY_PUMACTL[@pumactl] + keys.fetch(key) { |k| "--#{k.to_s.tr('_', '-')}" } + end + def run_puma_command!(cmd) - Net::HTTP.get build_uri(cmd) + if pumactl + Kernel.system build_command(cmd) + else + Net::HTTP.get build_uri(cmd) + end return true rescue Errno::ECONNREFUSED => e # server may not have been started correctly. @@ -80,12 +101,22 @@ def build_uri(cmd) URI "http://#{control_url}/#{cmd}?token=#{control_token}" end - def nix_start_cmd - %{sh -c 'cd #{Dir.pwd} && puma #{cmd_opts} &'} + def build_command(cmd) + puma_cmd = "#{pumactl ? 'pumactl' : 'puma'} #{cmd_opts} #{cmd if pumactl}" + background = cmd == 'start' + if in_windows_cmd? + windows_cmd(puma_cmd, background) + else + nix_cmd(puma_cmd, background) + end + end + + def nix_cmd(puma_cmd, background = false) + %(sh -c 'cd #{Dir.pwd} && #{puma_cmd} #{'&' if background}') end - def windows_start_cmd - %{cd "#{Dir.pwd}" && start "" /B cmd /C "puma #{cmd_opts}"} + def windows_cmd(puma_cmd, background = false) + %(cd "#{Dir.pwd}" && #{'start "" /B' if background} cmd /C "#{puma_cmd}") end def in_windows_cmd? diff --git a/spec/lib/guard/puma/runner_spec.rb b/spec/lib/guard/puma/runner_spec.rb index 46f4792..f479858 100644 --- a/spec/lib/guard/puma/runner_spec.rb +++ b/spec/lib/guard/puma/runner_spec.rb @@ -1,13 +1,12 @@ require 'spec_helper' require 'guard/puma/runner' -require 'pry' describe Guard::PumaRunner do let(:runner) { Guard::PumaRunner.new(options) } let(:environment) { 'development' } let(:port) { 4000 } - let(:default_options) { { :environment => environment, :port => port } } + let(:default_options) { { environment: environment, port: port } } let(:options) { default_options } describe "#initialize" do @@ -16,39 +15,61 @@ end end - %w(halt restart).each do |cmd| + %w[halt restart].each do |cmd| describe cmd do - before do - allow(runner).to receive(:build_uri).with(cmd).and_return(uri) + context "without pumactl" do + let(:options) { { pumactl: false } } + + let(:uri) { + URI( + "http://#{runner.control_url}/#{cmd}?token=#{runner.control_token}" + ) + } + + it "#{cmd}s" do + expect(Net::HTTP).to receive(:get).with(uri).once + runner.public_send(cmd) + end end - let(:uri) { URI("http://#{runner.control_url}/#{cmd}?token=#{runner.control_token}") } - it "#{cmd}s" do - expect(Net::HTTP).to receive(:get).with(uri).once - runner.send(cmd.intern) + + context "with pumactl" do + let(:options) { { pumactl: true } } + + before do + allow(runner).to receive(:in_windows_cmd?).and_return(false) + end + + let(:command) { + %(sh -c 'cd #{Dir.pwd} && pumactl #{runner.cmd_opts} #{cmd} ') + } + + it "#{cmd}s" do + expect(Kernel).to receive(:system).with(command).once + runner.public_send(cmd) + end end end end - describe '#start' do + describe "#start" do context "when on Windows" do before do allow(runner).to receive(:in_windows_cmd?).and_return(true) - allow(runner).to receive(:windows_start_cmd).and_return("echo 'windows'") end it "runs the Windows command" do - expect(Kernel).to receive(:system).with("echo 'windows'") + expect(Kernel).to receive(:system).with(%r{cmd /C ".+"}) runner.start end end context "when on *nix" do before do - allow(runner).to receive(:nix_start_cmd).and_return("echo 'nix'") + allow(runner).to receive(:in_windows_cmd?).and_return(false) end it "runs the *nix command" do - expect(Kernel).to receive(:system).with("echo 'nix'") + expect(Kernel).to receive(:system).with(/sh -c '.+'/) runner.start end end @@ -69,45 +90,127 @@ let(:command) { runner.start } context "with config" do - let(:options) {{ :config => path }} let(:path) { "/tmp/elephants" } let(:environment) { "special_dev" } - it "adds path to command" do - expect(runner.cmd_opts).to match("--config #{path}") - end - context "and additional options" do - let(:options) {{ :config => path, :port => "4000", quiet: false, :environment => environment }} - it "assumes options are set in config" do + context "without pumactl" do + let(:options) { { config: path, pumactl: false } } + + it "adds path to command" do expect(runner.cmd_opts).to match("--config #{path}") - expect(runner.cmd_opts).to match(/--control-token [0-9a-f]{10,}/) - expect(runner.cmd_opts).to match("--control tcp") - expect(runner.cmd_opts).to match("--environment #{environment}") + end + + context "and additional options" do + let(:options) { + { + config: path, port: "4000", + quiet: false, environment: environment + } + } + + it "assumes options are set in config" do + expect(runner.cmd_opts).to match("--config #{path}") + expect(runner.cmd_opts).to match(/--control-token [0-9a-f]{10,}/) + expect(runner.cmd_opts).to match("--control tcp") + expect(runner.cmd_opts).to match("--environment #{environment}") + end + end + end + + context "with pumactl" do + let(:options) { { config: path, pumactl: true } } + + it "adds path to command" do + expect(runner.cmd_opts).to match("--config-file #{path}") + end + + context "and additional options" do + let(:options) { + { + pumactl: true, + config: path, port: "4000", + quiet: false + } + } + + it "assumes options are set in config" do + expect(runner.cmd_opts).to match("--config-file #{path}") + expect(runner.cmd_opts).to match(/--control-token [0-9a-f]{10,}/) + expect(runner.cmd_opts).to match("--control-url tcp") + end end end end context "with bind" do - let(:options) {{ :bind => uri }} let(:uri) { "tcp://foo" } - it "adds uri option to command" do - expect(runner.cmd_opts).to match("--bind #{uri}") + + context "without pumactl" do + let(:options) { { pumactl: false, bind: uri } } + + it "adds uri option to command" do + expect(runner.cmd_opts).to match("--bind #{uri}") + end + end + + context "with pumactl" do + let(:options) { { pumactl: true, bind: uri } } + + it "raises ArgumentError about incompatible options" do + expect(Guard::Compat::UI).to receive(:warning).with(/bind.+pumactl/) + runner.cmd_opts + end end end context "with control_token" do - let(:options) {{ :control_token => token }} let(:token) { "imma-token" } + let(:options) { { control_token: token } } + it "adds token to command" do expect(runner.cmd_opts).to match(/--control-token #{token}/) end end context "with threads" do - let(:options) {{ :threads => threads }} let(:threads) { "13:42" } - it "adds path to command" do - expect(runner.cmd_opts).to match("--threads #{threads}") + + context "without pumactl" do + let(:options) { { pumactl: false, threads: threads } } + + it "adds threads option to command" do + expect(runner.cmd_opts).to match("--threads #{threads}") + end + end + + context "with pumactl" do + let(:options) { { pumactl: true, threads: threads } } + + it "raises ArgumentError about incompatible options" do + expect(Guard::Compat::UI).to receive(:warning).with(/threads.+pumactl/) + runner.cmd_opts + end + end + end + + context "with environment" do + let(:environment) { "development" } + + context "without pumactl" do + let(:options) { { pumactl: false, environment: environment } } + + it "adds environment option to command" do + expect(runner.cmd_opts).to match("--environment #{environment}") + end + end + + context "with pumactl" do + let(:options) { { pumactl: true, environment: environment } } + + it "warns about incompatible options" do + expect(Guard::Compat::UI).to receive(:warning).with(/environment.+pumactl/) + runner.cmd_opts + end end end end diff --git a/spec/lib/guard/puma_spec.rb b/spec/lib/guard/puma_spec.rb index 1ac9a23..7bcdc96 100644 --- a/spec/lib/guard/puma_spec.rb +++ b/spec/lib/guard/puma_spec.rb @@ -57,19 +57,20 @@ end describe '#start' do - - context 'start on start' do + context "start on start" do it "runs startup" do - expect(guard).to receive(:start).once + expect(guard.runner).to receive(:start).once + expect(Guard::Compat::UI).to receive(:info).with(/Puma starting/) guard.start end end - context 'no start on start' do - let(:options) { { :start_on_start => false } } + context "no start on start" do + let(:options) { { start_on_start: false } } - it "shows the right message and not run startup" do - expect(guard.runner).to receive(:start).never + it "doesn't show the message and not run startup" do + expect(guard.runner).not_to receive(:start) + expect(Guard::Compat::UI).not_to receive(:info).with(/Puma starting/) guard.start end end @@ -81,42 +82,45 @@ context "when no config option set" do it "contains port" do - expect(Guard::UI).to receive(:info).with(/starting on port 4000/) + expect(Guard::Compat::UI).to receive(:info) + .with(/starting on port 4000/) guard.start end end context "when config option set" do - let(:options) { { :config => 'config.rb' } } + let(:options) { { config: 'config.rb' } } it "doesn't contain port" do - expect(Guard::UI).to receive(:info).with(/starting/) + expect(Guard::Compat::UI).to receive(:info).with(/starting/) guard.start end end end end - describe '#reload' do + describe "#reload" do + let(:zero_restart_timeout) { { restart_timeout: 0 } } + let(:options) { zero_restart_timeout } before do - expect(Guard::UI).to receive(:info).with('Restarting Puma...') - expect(Guard::UI).to receive(:info).with('Puma restarted') allow(guard.runner).to receive(:restart).and_return(true) + allow_any_instance_of(Guard::PumaRunner).to receive(:halt) end - let(:runner_stub) { allow_any_instance_of(Guard::PumaRunner).to receive(:halt) } - context "with default options" do it "restarts and show the message" do - expect(Guard::Notifier).to receive(:notify).with( + expect(Guard::Compat::UI).to receive(:info).with('Restarting Puma...') + expect(Guard::Compat::UI).to receive(:info).with('Puma restarted') + + expect(Guard::Compat::UI).to receive(:notify).with( /restarting on port 4000/, - hash_including(:title => "Restarting Puma...", :image => :pending) + hash_including(title: "Restarting Puma...", image: :pending) ) - expect(Guard::Notifier).to receive(:notify).with( + expect(Guard::Compat::UI).to receive(:notify).with( "Puma restarted on port 4000.", - hash_including(:title => "Puma restarted!", :image => :success) + hash_including(title: "Puma restarted!", image: :success) ) guard.reload @@ -124,17 +128,20 @@ end context "with config option set" do - let(:options) { { :config => "config.rb" } } + let(:options) { { config: "config.rb" }.merge!(zero_restart_timeout) } it "restarts and show the message" do - expect(Guard::Notifier).to receive(:notify).with( + expect(Guard::Compat::UI).to receive(:info).with('Restarting Puma...') + expect(Guard::Compat::UI).to receive(:info).with('Puma restarted') + + expect(Guard::Compat::UI).to receive(:notify).with( /restarting/, - hash_including(:title => "Restarting Puma...", :image => :pending) + hash_including(title: "Restarting Puma...", image: :pending) ) - expect(Guard::Notifier).to receive(:notify).with( + expect(Guard::Compat::UI).to receive(:notify).with( "Puma restarted.", - hash_including(:title => "Puma restarted!", :image => :success) + hash_including(title: "Puma restarted!", image: :success) ) guard.reload @@ -142,49 +149,95 @@ end context "with custom :notifications option" do - let(:options) { { :notifications => [:restarted] } } + let(:options) { + { notifications: [:restarted] }.merge!(zero_restart_timeout) + } it "restarts and show the message only about restarted" do - expect(Guard::Notifier).not_to receive(:notify).with(/restarting/) - expect(Guard::Notifier).to receive(:notify).with(/restarted/, kind_of(Hash)) + allow(Guard::Compat::UI).to receive(:info) + + expect(Guard::Compat::UI).not_to receive(:notify).with(/restarting/) + expect(Guard::Compat::UI).to receive(:notify) + .with(/restarted/, kind_of(Hash)) guard.reload end end context "with empty :notifications option" do - let(:options) { { :notifications => [] } } + let(:options) { { notifications: [] }.merge!(zero_restart_timeout) } it "restarts and doesn't show the message" do - expect(Guard::Notifier).not_to receive(:notify) + allow(Guard::Compat::UI).to receive(:info) + + expect(Guard::Compat::UI).not_to receive(:notify) guard.reload end end + context "with :restart_timeout option" do + let(:restart_timeout) { 1.0 } + let(:options) { { restart_timeout: restart_timeout } } + + before { sleep restart_timeout } + + it "doesn't restarts during restart timeout" do + allow(Guard::Compat::UI).to receive(:info) + allow(Guard::Compat::UI).to receive(:notify) + + expect(guard.runner).to receive(:restart).twice + + guard.reload + sleep restart_timeout / 2 + guard.reload + sleep restart_timeout + guard.reload + end + end end - describe '#stop' do + describe "#stop" do context "with default options" do it "stops correctly with notification" do - expect(Guard::Notifier).to receive(:notify).with('Until next time...', anything) + expect(Guard::Compat::UI).to receive(:notify) + .with('Until next time...', anything) expect(guard.runner).to receive(:halt).once guard.stop end end context "with custom :notifications option" do - let(:options) { { :notifications => [] } } + let(:options) { { notifications: [] } } it "stops correctly without notification" do - expect(Guard::Notifier).not_to receive(:notify) + expect(Guard::Compat::UI).not_to receive(:notify) expect(guard.runner).to receive(:halt).once guard.stop end end + + context "start on start" do + it "stops correctly with notification" do + expect(guard.runner).to receive(:halt).once + expect(Guard::Compat::UI).to receive(:notify) + .with('Until next time...', anything) + guard.stop + end + end + + context "no start on start" do + let(:options) { { start_on_start: false } } + + it "doesn't show the message and doesn't halt" do + expect(guard.runner).not_to receive(:halt) + expect(Guard::Compat::UI).not_to receive(:notify) + guard.stop + end + end end - describe '#run_on_changes' do + describe "#run_on_changes" do it "reloads on change" do expect(guard).to receive(:reload).once guard.run_on_changes([]) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5e260ef..be1e950 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,4 +11,5 @@ config.run_all_when_everything_filtered = true config.filter_run :focus config.mock_with :rspec + config.color = true end