45f28115199568f38e5e6975ed89d96dceb75f59
[god.git] / lib / god / process.rb
blob45f28115199568f38e5e6975ed89d96dceb75f59
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     def pid
129       contents = File.read(self.pid_file).strip rescue ''
130       real_pid = contents =~ /^\d+$/ ? contents.to_i : nil
131       
132       if real_pid
133         @pid = real_pid
134         real_pid
135       else
136         @pid
137       end
138     end
139     
140     def start!
141       call_action(:start)
142     end
143     
144     def stop!
145       call_action(:stop)
146     end
147     
148     def restart!
149       call_action(:restart)
150     end
151     
152     def spawn(command)
153       fork do
154         ::Process.setsid
155         ::Process::Sys.setgid(Etc.getgrnam(self.gid).gid) if self.gid
156         ::Process::Sys.setuid(Etc.getpwnam(self.uid).uid) if self.uid
157         Dir.chdir "/"
158         $0 = command
159         STDIN.reopen "/dev/null"
160         STDOUT.reopen self.log, "a"
161         STDERR.reopen STDOUT
162         
163         # close any other file descriptors
164         3.upto(256){|fd| IO::new(fd).close rescue nil}
165         
166         exec command unless command.empty?
167       end
168     end
169     
170     def call_action(action)
171       command = send(action)
172       
173       if action == :stop && command.nil?
174         pid = File.read(self.pid_file).strip.to_i
175         name = self.name
176         command = lambda do
177           applog(self, :info, "#{self.name} stop: default lambda killer")
178           
179           ::Process.kill('HUP', pid) rescue nil
180           
181           # Poll to see if it's dead
182           5.times do
183             begin
184               ::Process.kill(0, pid)
185             rescue Errno::ESRCH
186               # It died. Good.
187               return
188             end
189             
190             sleep 1
191           end
192           
193           ::Process.kill('KILL', pid) rescue nil
194         end
195       end
196             
197       if command.kind_of?(String)
198         pid = nil
199         
200         if @tracking_pid
201           # double fork god-daemonized processes
202           # we don't want to wait for them to finish
203           r, w = IO.pipe
204           begin
205             opid = fork do
206               STDOUT.reopen(w)
207               r.close
208               pid = self.spawn(command)
209               puts pid.to_s # send pid back to forker
210             end
211             
212             ::Process.waitpid(opid, 0)
213             w.close
214             pid = r.gets.chomp
215           ensure
216             # make sure the file descriptors get closed no matter what
217             r.close rescue nil
218             w.close rescue nil
219           end
220         else
221           # single fork self-daemonizing processes
222           # we want to wait for them to finish
223           pid = self.spawn(command)
224           ::Process.waitpid(pid, 0)
225         end
226         
227         if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
228           File.open(default_pid_file, 'w') do |f|
229             f.write pid
230           end
231           
232           @tracking_pid = true
233           @pid_file = default_pid_file
234         end
235       elsif command.kind_of?(Proc)
236         # lambda command
237         command.call
238       else
239         raise NotImplementedError
240       end
241     end
242     
243     def default_pid_file
244       File.join(God.pid_file_directory, "#{self.name}.pid")
245     end
246   end