more diagnostics
[god.git] / lib / god / process.rb
blob974afcc1ba303e2dc26ab3259be74b802f0cdafd
1 require 'fileutils'
3 module God
4   class Process
5     WRITES_PID = [:start, :restart]
6     
7     attr_accessor :name, :uid, :gid, :log, :start, :stop, :restart
8     
9     def initialize
10       self.log = '/dev/null'
11       
12       @pid_file = nil
13       @tracking_pid = true
14       @user_log = false
15       @pid = nil
16     end
17     
18     def alive?
19       begin
20         pid = File.read(self.pid_file).strip.to_i
21         System::Process.new(pid).exists?
22       rescue Errno::ENOENT
23         false
24       end
25     end
26     
27     def file_writable?(file)
28       pid = fork do
29         ::Process::Sys.setgid(Etc.getgrnam(self.gid).gid) if self.gid
30         ::Process::Sys.setuid(Etc.getpwnam(self.uid).uid) if self.uid
31         
32         File.writable?(file) ? exit(0) : exit(1)
33       end
34       
35       wpid, status = ::Process.waitpid2(pid)
36       status.exitstatus == 0 ? true : false
37     end
38     
39     def valid?
40       # determine if we're tracking pid or not
41       self.pid_file
42       
43       valid = true
44       
45       # a start command must be specified
46       if self.start.nil?
47         valid = false
48         applog(self, :error, "No start command was specified")
49       end
50       
51       # self-daemonizing processes must specify a stop command
52       if !@tracking_pid && self.stop.nil?
53         valid = false
54         applog(self, :error, "No stop command was specified")
55       end
56       
57       # uid must exist if specified
58       if self.uid
59         begin
60           Etc.getpwnam(self.uid)
61         rescue ArgumentError
62           valid = false
63           applog(self, :error, "UID for '#{self.uid}' does not exist")
64         end
65       end
66       
67       # gid must exist if specified
68       if self.gid
69         begin
70           Etc.getgrnam(self.gid)
71         rescue ArgumentError
72           valid = false
73           applog(self, :error, "GID for '#{self.gid}' does not exist")
74         end
75       end
76       
77       # pid dir must exist if specified
78       if !@tracking_pid && !File.exist?(File.dirname(self.pid_file))
79         valid = false
80         applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' does not exist")
81       end
82       
83       # pid dir must be writable if specified
84       if !@tracking_pid && File.exist?(File.dirname(self.pid_file)) && !file_writable?(File.dirname(self.pid_file))
85         valid = false
86         applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' is not writable by #{self.uid || Etc.getlogin}")
87       end
88       
89       # log dir must exist
90       if !File.exist?(File.dirname(self.log))
91         valid = false
92         applog(self, :error, "Log directory '#{File.dirname(self.log)}' does not exist")
93       end
94       
95       # log file or dir must be writable
96       if File.exist?(self.log)
97         unless file_writable?(self.log)
98           valid = false
99           applog(self, :error, "Log file '#{self.log}' exists but is not writable by #{self.uid || Etc.getlogin}")
100         end
101       else
102         unless file_writable?(File.dirname(self.log))
103           valid = false
104           applog(self, :error, "Log directory '#{File.dirname(self.log)}' is not writable by #{self.uid || Etc.getlogin}")
105         end
106       end
107       
108       valid
109     end
110     
111     # DON'T USE THIS INTERNALLY. Use the instance variable. -- Kev
112     # No really, trust me. Use the instance variable.
113     def pid_file=(value)
114       # if value is nil, do the right thing
115       if value
116         @tracking_pid = false
117       else
118         @tracking_pid = true
119       end
120       
121       @pid_file = value
122     end
123     
124     def pid_file
125       @pid_file ||= default_pid_file
126     end
127     
128     # Fetch the PID from pid_file. If the pid_file does not
129     # exist, then use the PID from the last time it was read.
130     # If it has never been read, then return nil.
131     #
132     # Returns Integer(pid) or nil
133     def pid
134       contents = File.read(self.pid_file).strip rescue ''
135       real_pid = contents =~ /^\d+$/ ? contents.to_i : nil
136       
137       if real_pid
138         @pid = real_pid
139         real_pid
140       else
141         @pid
142       end
143     end
144     
145     def start!
146       call_action(:start)
147     end
148     
149     def stop!
150       call_action(:stop)
151     end
152     
153     def restart!
154       call_action(:restart)
155     end
156     
157     def spawn(command)
158       fork do
159         ::Process.setsid
160         ::Process::Sys.setgid(Etc.getgrnam(self.gid).gid) if self.gid
161         ::Process::Sys.setuid(Etc.getpwnam(self.uid).uid) if self.uid
162         Dir.chdir "/"
163         $0 = command
164         STDIN.reopen "/dev/null"
165         STDOUT.reopen self.log, "a"
166         STDERR.reopen STDOUT
167         
168         # close any other file descriptors
169         3.upto(256){|fd| IO::new(fd).close rescue nil}
170         
171         exec command unless command.empty?
172       end
173     end
174     
175     def call_action(action)
176       command = send(action)
177       
178       if action == :stop && command.nil?
179         pid = File.read(self.pid_file).strip.to_i
180         name = self.name
181         command = lambda do
182           applog(self, :info, "#{self.name} stop: default lambda killer")
183           
184           ::Process.kill('HUP', pid) rescue nil
185           
186           # Poll to see if it's dead
187           5.times do
188             begin
189               ::Process.kill(0, pid)
190             rescue Errno::ESRCH
191               # It died. Good.
192               return
193             end
194             
195             sleep 1
196           end
197           
198           ::Process.kill('KILL', pid) rescue nil
199         end
200       end
201             
202       if command.kind_of?(String)
203         pid = nil
204         
205         if @tracking_pid
206           # double fork god-daemonized processes
207           # we don't want to wait for them to finish
208           r, w = IO.pipe
209           begin
210             opid = fork do
211               STDOUT.reopen(w)
212               r.close
213               pid = self.spawn(command)
214               puts pid.to_s # send pid back to forker
215             end
216             
217             ::Process.waitpid(opid, 0)
218             w.close
219             pid = r.gets.chomp
220           ensure
221             # make sure the file descriptors get closed no matter what
222             r.close rescue nil
223             w.close rescue nil
224           end
225         else
226           # single fork self-daemonizing processes
227           # we want to wait for them to finish
228           pid = self.spawn(command)
229           ::Process.waitpid(pid, 0)
230         end
231         
232         if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
233           File.open(default_pid_file, 'w') do |f|
234             f.write pid
235           end
236           
237           @tracking_pid = true
238           @pid_file = default_pid_file
239         end
240       elsif command.kind_of?(Proc)
241         # lambda command
242         command.call
243       else
244         raise NotImplementedError
245       end
246     end
247     
248     def default_pid_file
249       File.join(God.pid_file_directory, "#{self.name}.pid")
250     end
251   end