diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index c931184f1..ba7de156b 100644 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,182 +1,191 @@ require 'puppet/application' require 'puppet/scheduler' # Run periodic actions and a network server in a daemonized process. # # A Daemon has 3 parts: # * config reparse # * (optional) an agent that responds to #run # * (optional) a server that response to #stop, #start, and #wait_for_shutdown # # The config reparse will occur periodically based on Settings. The server will # be started and is expected to manage its own run loop (and so not block the # start call). The server will, however, still be waited for by using the # #wait_for_shutdown method. The agent is run periodically and a time interval # based on Settings. The config reparse will update this time interval when # needed. # # The Daemon is also responsible for signal handling, starting, stopping, # running the agent on demand, and reloading the entire process. It ensures # that only one Daemon is running by using a lockfile. # # @api private class Puppet::Daemon + SIGNAL_CHECK_INTERVAL = 5 + attr_accessor :agent, :server, :argv def initialize(pidfile, scheduler = Puppet::Scheduler::Scheduler.new()) @scheduler = scheduler @pidfile = pidfile + @signals = [] end 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("/") close_streams 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 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({:splay => false}) 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) + Puppet.notice "Caught #{signal}; storing #{method}" + @signals << 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 # Finally, loop forever running events - or, at least, until we exit. run_event_loop server.wait_for_shutdown if server end private # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile raise "Could not create PID file: #{@pidfile.file_path}" unless @pidfile.lock end # Remove the pid file for our daemon. def remove_pidfile @pidfile.unlock end def run_event_loop agent_run = Puppet::Scheduler.create_job(Puppet[:runinterval], Puppet[:splay], Puppet[:splaylimit]) do # Splay for the daemon is handled in the scheduler agent.run(:splay => false) end reparse_run = Puppet::Scheduler.create_job(Puppet[:filetimeout]) do Puppet.settings.reparse_config_files agent_run.run_interval = Puppet[:runinterval] if Puppet[:filetimeout] == 0 reparse_run.disable else reparse_run.run_interval = Puppet[:filetimeout] end end + signal_loop = Puppet::Scheduler.create_job(SIGNAL_CHECK_INTERVAL) do + while method = @signals.shift + Puppet.notice "Processing #{method}" + send(method) + end + end + reparse_run.disable if Puppet[:filetimeout] == 0 agent_run.disable unless agent - @scheduler.run_loop([reparse_run, agent_run]) + @scheduler.run_loop([reparse_run, agent_run, signal_loop]) end end - diff --git a/spec/unit/daemon_spec.rb b/spec/unit/daemon_spec.rb index b727d669c..a04ab7076 100755 --- a/spec/unit/daemon_spec.rb +++ b/spec/unit/daemon_spec.rb @@ -1,255 +1,253 @@ #! /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 class RecordingScheduler attr_reader :jobs def run_loop(jobs) @jobs = jobs end end let(:server) { stub("Server", :start => nil, :wait_for_shutdown => nil) } let(:agent) { Puppet::Agent.new(TestClient.new, false) } let(:pidfile) { stub("PidFile", :lock => true, :unlock => true, :file_path => 'fake.pid') } let(:scheduler) { RecordingScheduler.new } let(:daemon) { Puppet::Daemon.new(pidfile, scheduler) } before do daemon.stubs(:close_streams).returns nil end it "should reopen the Log logs when told to reopen logs" do Puppet::Util::Log.expects(:reopen) daemon.reopen_logs end let(:server) { stub("Server", :start => nil, :wait_for_shutdown => nil) } 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(:set_signal_traps) end it "should fail if it has neither agent nor server" do expect { daemon.start }.to raise_error(Puppet::DevError) end it "should create its pidfile" do pidfile.expects(:lock).returns(true) daemon.agent = agent daemon.start end it "should fail if it cannot lock" do pidfile.expects(:lock).returns(false) daemon.agent = agent expect { daemon.start }.to raise_error(RuntimeError, "Could not create PID file: #{pidfile.file_path}") end it "should start its server if one is configured" do daemon.server = server server.expects(:start) daemon.start end it "disables the reparse of configs if the filetimeout is 0" do Puppet[:filetimeout] = 0 daemon.agent = agent daemon.start scheduler.jobs[0].should_not be_enabled end it "disables the agent run when there is no agent" do Puppet[:filetimeout] = 0 daemon.server = server daemon.start scheduler.jobs[1].should_not be_enabled end it "waits for the server to shutdown when there is one" do daemon.server = server server.expects(:wait_for_shutdown) daemon.start end it "waits for the server to shutdown when there is one" do daemon.server = server server.expects(:wait_for_shutdown) daemon.start end end describe "when stopping" do before do 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.expects(:stop) daemon.server = 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 pidfile.expects(:unlock) 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 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).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