diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index 6681ad4bb..678fdb5d0 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,123 +1,123 @@ require 'sync' require 'puppet/application' # A general class for triggering a run of another # class. class Puppet::Agent require 'puppet/agent/locker' include Puppet::Agent::Locker require 'puppet/agent/disabler' include Puppet::Agent::Disabler attr_reader :client_class, :client, :splayed, :should_fork # Just so we can specify that we are "the" instance. def initialize(client_class, should_fork=true) @splayed = false @should_fork = should_fork @client_class = client_class end def needing_restart? Puppet::Application.restart_requested? end # Perform a run with our client. def run(client_options = {}) if running? Puppet.notice "Run of #{client_class} already in progress; skipping (#{lockfile_path} exists)" return end if disabled? Puppet.notice "Skipping run of #{client_class}; administratively disabled (Reason: '#{disable_message}');\nUse 'puppet agent --enable' to re-enable." return end result = nil block_run = Puppet::Application.controlled_run do - splay + splay client_options.fetch :splay, Puppet[:splay] result = run_in_fork(should_fork) do with_client do |client| begin client_args = client_options.merge(:pluginsync => Puppet[:pluginsync]) sync.synchronize { lock { client.run(client_args) } } rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not run #{client_class}: #{detail}") end end end true end Puppet.notice "Shutdown/restart in progress (#{Puppet::Application.run_status.inspect}); skipping run" unless block_run result end def stopping? Puppet::Application.stop_requested? end # Have we splayed already? def splayed? splayed end # Sleep when splay is enabled; else just return. - def splay - return unless Puppet[:splay] + def splay(do_splay = Puppet[:splay]) + return unless do_splay return if splayed? time = rand(Puppet[:splaylimit] + 1) Puppet.info "Sleeping for #{time} seconds (splay is enabled)" sleep(time) @splayed = true end def sync @sync ||= Sync.new end def run_in_fork(forking = true) return yield unless forking or Puppet.features.windows? child_pid = Kernel.fork do $0 = "puppet agent: applying configuration" begin exit(yield) rescue SystemExit exit(-1) rescue NoMemoryError exit(-2) end end exit_code = Process.waitpid2(child_pid) case exit_code[1].exitstatus when -1 raise SystemExit when -2 raise NoMemoryError end exit_code[1].exitstatus end private # Create and yield a client instance, keeping a reference # to it during the yield. def with_client begin @client = client_class.new rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not create instance of #{client_class}: #{detail}") return end yield @client ensure @client = nil end end diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index fa64146bb..1762c5a63 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,218 +1,218 @@ require 'puppet' require 'puppet/util/pidlock' require 'puppet/application' # A module that handles operations common to all daemons. This is included # into the Server and Client base classes. class Puppet::Daemon attr_accessor :agent, :server, :argv def daemonname Puppet.run_mode.name end # Put the daemon into the background. def daemonize if pid = fork Process.detach(pid) exit(0) end create_pidfile # Get rid of console logging Puppet::Util::Log.close(:console) Process.setsid Dir.chdir("/") end # Close stdin/stdout/stderr so that we can finish our transition into 'daemon' mode. # @return nil def self.close_streams() Puppet.debug("Closing streams for daemon mode") begin $stdin.reopen "/dev/null" $stdout.reopen "/dev/null", "a" $stderr.reopen $stdout Puppet::Util::Log.reopen Puppet.debug("Finished closing streams for daemon mode") rescue => detail Puppet.err "Could not start #{Puppet.run_mode.name}: #{detail}" Puppet::Util::replace_file("/tmp/daemonout", 0644) do |f| f.puts "Could not start #{Puppet.run_mode.name}: #{detail}" end exit(12) end end # Convenience signature for calling Puppet::Daemon.close_streams def close_streams() Puppet::Daemon.close_streams end # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile Puppet::Util.synchronize_on(Puppet.run_mode.name,Sync::EX) do raise "Could not create PID file: #{pidfile}" unless Puppet::Util::Pidlock.new(pidfile).lock end end # Provide the path to our pidfile. def pidfile Puppet[:pidfile] end def reexec raise Puppet::DevError, "Cannot reexec unless ARGV arguments are set" unless argv command = $0 + " " + argv.join(" ") Puppet.notice "Restarting with '#{command}'" stop(:exit => false) exec(command) end def reload return unless agent if agent.running? Puppet.notice "Not triggering already-running agent" return end - agent.run + agent.run({:splay => false}) end # Remove the pid file for our daemon. def remove_pidfile Puppet::Util.synchronize_on(Puppet.run_mode.name,Sync::EX) do Puppet::Util::Pidlock.new(pidfile).unlock end end def restart Puppet::Application.restart! reexec unless agent and agent.running? end def reopen_logs Puppet::Util::Log.reopen end # Trap a couple of the main signals. This should probably be handled # in a way that anyone else can register callbacks for traps, but, eh. def set_signal_traps signals = {:INT => :stop, :TERM => :stop } # extended signals not supported under windows signals.update({:HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs }) unless Puppet.features.microsoft_windows? signals.each do |signal, method| Signal.trap(signal) do Puppet.notice "Caught #{signal}; calling #{method}" send(method) end end end # Stop everything def stop(args = {:exit => true}) Puppet::Application.stop! server.stop if server remove_pidfile Puppet::Util::Log.close_all exit if args[:exit] end def start set_signal_traps create_pidfile raise Puppet::DevError, "Daemons must have an agent, server, or both" unless agent or server # Start the listening server, if required. server.start if server # now that the server has started, we've waited just about as long as possible to close # our streams and become a "real" daemon process. This is in hopes of allowing # errors to have the console available as a fallback for logging for as long as # possible. close_streams if Puppet[:daemonize] # Finally, loop forever running events - or, at least, until we exit. run_event_loop end def run_event_loop # Now, we loop waiting for either the configuration file to change, or the # next agent run to be due. Fun times. # # We want to trigger the reparse if 15 seconds passed since the previous # wakeup, and the agent run if Puppet[:runinterval] seconds have passed # since the previous wakeup. # # We always want to run the agent on startup, so it was always before now. # Because 0 means "continuously run", `to_i` does the right thing when the # input is strange or badly formed by returning 0. Integer will raise, # which we don't want, and we want to protect against -1 or below. next_agent_run = 0 agent_run_interval = [Puppet[:runinterval], 0].max # We may not want to reparse; that can be disable. Fun times. next_reparse = 0 reparse_interval = Puppet[:filetimeout] loop do now = Time.now.to_i # We set a default wakeup of "one hour from now", which will # recheck everything at a minimum every hour. Just in case something in # the math messes up or something; it should be inexpensive enough to # wake once an hour, then go back to sleep after doing nothing, if # someone only wants listen mode. next_event = now + 60 * 60 # Handle reparsing of configuration files, if desired and required. # `reparse` will just check if the action is required, and would be # better named `reparse_if_changed` instead. if reparse_interval > 0 and now >= next_reparse Puppet.settings.reparse_config_files # The time to the next reparse might have changed, so recalculate # now. That way we react dynamically to reconfiguration. reparse_interval = Puppet[:filetimeout] # Set up the next reparse check based on the new reparse_interval. if reparse_interval > 0 next_reparse = now + reparse_interval next_event > next_reparse and next_event = next_reparse end # We should also recalculate the agent run interval, and adjust the # next time it is scheduled to run, just in case. In the event that # we made no change the result will be a zero second adjustment. new_run_interval = [Puppet[:runinterval], 0].max next_agent_run += agent_run_interval - new_run_interval agent_run_interval = new_run_interval end # Handle triggering another agent run. This will block the next check # for configuration reparsing, which is a desired and deliberate # behaviour. You should not change that. --daniel 2012-02-21 if agent and now >= next_agent_run agent.run # Set up the next agent run time next_agent_run = now + agent_run_interval next_event > next_agent_run and next_event = next_agent_run end # Finally, an interruptable able sleep until the next scheduled event. how_long = next_event - now how_long > 0 and select([], [], [], how_long) end end end diff --git a/spec/unit/daemon_spec.rb b/spec/unit/daemon_spec.rb index 4637e510c..7a5581c51 100755 --- a/spec/unit/daemon_spec.rb +++ b/spec/unit/daemon_spec.rb @@ -1,269 +1,269 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/daemon' require 'puppet/agent' def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end class TestClient def lockfile_path "/dev/null" end end describe Puppet::Daemon, :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files before do # Forking agent not needed here @agent = Puppet::Agent.new(TestClient.new, false) @daemon = Puppet::Daemon.new @daemon.stubs(:close_streams).returns nil end it "should be able to manage an agent" do @daemon.should respond_to(:agent) end it "should be able to manage a network server" do @daemon.should respond_to(:server) end it "should reopen the Log logs when told to reopen logs" do Puppet::Util::Log.expects(:reopen) @daemon.reopen_logs end describe "when setting signal traps" do signals = {:INT => :stop, :TERM => :stop } signals.update({:HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs}) unless Puppet.features.microsoft_windows? signals.each do |signal, method| it "should log and call #{method} when it receives #{signal}" do Signal.expects(:trap).with(signal).yields Puppet.expects(:notice) @daemon.expects(method) @daemon.set_signal_traps end end end describe "when starting" do before do @daemon.stubs(:create_pidfile) @daemon.stubs(:set_signal_traps) @daemon.stubs(:run_event_loop) end it "should fail if it has neither agent nor server" do lambda { @daemon.start }.should raise_error(Puppet::DevError) end it "should create its pidfile" do @daemon.agent = @agent @daemon.expects(:create_pidfile) @daemon.start end it "should start its server if one is configured" do server = mock 'server' server.expects(:start) @daemon.stubs(:server).returns server @daemon.start end end describe "when stopping" do before do @daemon.stubs(:remove_pidfile) Puppet::Util::Log.stubs(:close_all) # to make the global safe to mock, set it to a subclass of itself, # then restore it in an after pass without_warnings { Puppet::Application = Class.new(Puppet::Application) } end after do # restore from the superclass so we lose the stub garbage without_warnings { Puppet::Application = Puppet::Application.superclass } end it "should stop its server if one is configured" do server = mock 'server' server.expects(:stop) @daemon.stubs(:server).returns server expect { @daemon.stop }.to exit_with 0 end it 'should request a stop from Puppet::Application' do Puppet::Application.expects(:stop!) expect { @daemon.stop }.to exit_with 0 end it "should remove its pidfile" do @daemon.expects(:remove_pidfile) expect { @daemon.stop }.to exit_with 0 end it "should close all logs" do Puppet::Util::Log.expects(:close_all) expect { @daemon.stop }.to exit_with 0 end it "should exit unless called with ':exit => false'" do expect { @daemon.stop }.to exit_with 0 end it "should not exit if called with ':exit => false'" do @daemon.stop :exit => false end end describe "when creating its pidfile" do it "should use an exclusive mutex" do Puppet.run_mode.expects(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @daemon.create_pidfile end it "should lock the pidfile using the Pidlock class" do pidfile = mock 'pidfile' Puppet.run_mode.expects(:name).returns "eh" Puppet[:pidfile] = make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile pidfile.expects(:lock).returns true @daemon.create_pidfile end it "should fail if it cannot lock" do pidfile = mock 'pidfile' Puppet.run_mode.expects(:name).returns "eh" Puppet[:pidfile] = make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile pidfile.expects(:lock).returns false lambda { @daemon.create_pidfile }.should raise_error end end describe "when removing its pidfile" do it "should use an exclusive mutex" do Puppet.run_mode.expects(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @daemon.remove_pidfile end it "should do nothing if the pidfile is not present" do pidfile = mock 'pidfile', :unlock => false Puppet[:pidfile] = make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile @daemon.remove_pidfile end it "should unlock the pidfile using the Pidlock class" do pidfile = mock 'pidfile', :unlock => true Puppet[:pidfile] = make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile @daemon.remove_pidfile end end describe "when reloading" do it "should do nothing if no agent is configured" do @daemon.reload end it "should do nothing if the agent is running" do @agent.expects(:running?).returns true @daemon.agent = @agent @daemon.reload end it "should run the agent if one is available and it is not running" do @agent.expects(:running?).returns false - @agent.expects :run + @agent.expects(:run).with({:splay => false}) @daemon.agent = @agent @daemon.reload end end describe "when restarting" do before do without_warnings { Puppet::Application = Class.new(Puppet::Application) } end after do without_warnings { Puppet::Application = Puppet::Application.superclass } end it 'should set Puppet::Application.restart!' do Puppet::Application.expects(:restart!) @daemon.stubs(:reexec) @daemon.restart end it "should reexec itself if no agent is available" do @daemon.expects(:reexec) @daemon.restart end it "should reexec itself if the agent is not running" do @agent.expects(:running?).returns false @daemon.agent = @agent @daemon.expects(:reexec) @daemon.restart end end describe "when reexecing it self" do before do @daemon.stubs(:exec) @daemon.stubs(:stop) end it "should fail if no argv values are available" do @daemon.expects(:argv).returns nil lambda { @daemon.reexec }.should raise_error(Puppet::DevError) end it "should shut down without exiting" do @daemon.argv = %w{foo} @daemon.expects(:stop).with(:exit => false) @daemon.reexec end it "should call 'exec' with the original executable and arguments" do @daemon.argv = %w{foo} @daemon.expects(:exec).with($0 + " foo") @daemon.reexec end end end