add host to notifications; check dir permissions on startup
[god.git] / lib / god / process.rb
blob4db1fc81ad0329014764c55035d8ac25a7c828d7
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     end
15     
16     def alive?
17       begin
18         pid = File.read(self.pid_file).strip.to_i
19         System::Process.new(pid).exists?
20       rescue Errno::ENOENT
21         false
22       end
23     end
24     
25     def file_writable?(file)
26       pid = fork do
27         ::Process::Sys.setgid(Etc.getgrnam(self.gid).gid) if self.gid
28         ::Process::Sys.setuid(Etc.getpwnam(self.uid).uid) if self.uid
29         
30         File.writable?(file) ? exit(0) : exit(1)
31       end
32       
33       wpid, status = ::Process.waitpid2(pid)
34       status.exitstatus == 0 ? true : false
35     end
36     
37     def valid?
38       # determine if we're tracking pid or not
39       self.pid_file
40       
41       valid = true
42       
43       # a start command must be specified
44       if self.start.nil?
45         valid = false
46         LOG.log(self, :error, "No start command was specified")
47       end
48       
49       # self-daemonizing processes must specify a stop command
50       if !@tracking_pid && self.stop.nil?
51         valid = false
52         LOG.log(self, :error, "No stop command was specified")
53       end
54       
55       # uid must exist if specified
56       if self.uid
57         begin
58           Etc.getpwnam(self.uid)
59         rescue ArgumentError
60           valid = false
61           LOG.log(self, :error, "UID for '#{self.uid}' does not exist")
62         end
63       end
64       
65       # gid must exist if specified
66       if self.gid
67         begin
68           Etc.getgrnam(self.gid)
69         rescue ArgumentError
70           valid = false
71           LOG.log(self, :error, "GID for '#{self.gid}' does not exist")
72         end
73       end
74       
75       # pid dir must exist if specified
76       if !@tracking_pid && !File.exist?(File.dirname(self.pid_file))
77         valid = false
78         LOG.log(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' does not exist")
79       end
80       
81       # pid dir must be writable if specified
82       if !@tracking_pid && !file_writable?(File.dirname(self.pid_file))
83         valid = false
84         LOG.log(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' is not writable by #{self.uid || Etc.getlogin}")
85       end
86       
87       # log dir must exist if specified
88       if self.log && !File.exist?(File.dirname(self.log))
89         valid = false
90         LOG.log(self, :error, "Log directory '#{File.dirname(self.log)}' does not exist")
91       end
92       
93       # log dir must be writable if specified
94       if self.log && !file_writable?(File.dirname(self.log))
95         valid = false
96         LOG.log(self, :error, "Log directory '#{File.dirname(self.log)}' is not writable by #{self.uid || Etc.getlogin}")
97       end
98       
99       valid
100     end
101     
102     # DON'T USE THIS INTERNALLY. Use the instance variable. -- Kev
103     # No really, trust me. Use the instance variable.
104     def pid_file=(value)
105       # if value is nil, do the right thing
106       if value
107         @tracking_pid = false
108       else
109         @tracking_pid = true
110       end
111       
112       @pid_file = value
113     end
114     
115     def pid_file
116       @pid_file ||= default_pid_file
117     end
118     
119     def start!
120       call_action(:start)
121     end
122     
123     def stop!
124       call_action(:stop)
125     end
126     
127     def restart!
128       call_action(:restart)
129     end
130     
131     def spawn(command)
132       fork do
133         ::Process.setsid
134         ::Process::Sys.setgid(Etc.getgrnam(self.gid).gid) if self.gid
135         ::Process::Sys.setuid(Etc.getpwnam(self.uid).uid) if self.uid
136         Dir.chdir "/"
137         $0 = command
138         STDIN.reopen "/dev/null"
139         STDOUT.reopen self.log, "a"
140         STDERR.reopen STDOUT
141         
142         exec command unless command.empty?
143       end
144     end
145     
146     def call_action(action)
147       command = send(action)
148       
149       if action == :stop && command.nil?
150         pid = File.read(self.pid_file).strip.to_i
151         name = self.name
152         command = lambda do
153           LOG.log(self, :info, "#{self.name} stop: default lambda killer")
154           
155           ::Process.kill('HUP', pid) rescue nil
156           
157           # Poll to see if it's dead
158           5.times do
159             begin
160               ::Process.kill(0, pid)
161             rescue Errno::ESRCH
162               # It died. Good.
163               return
164             end
165             
166             sleep 1
167           end
168           
169           ::Process.kill('KILL', pid) rescue nil
170         end
171       end
172             
173       if command.kind_of?(String)
174         pid = nil
175         
176         if @tracking_pid
177           # double fork god-daemonized processes
178           # we don't want to wait for them to finish
179           r, w = IO.pipe
180           opid = fork do
181             STDOUT.reopen(w)
182             r.close
183             pid = self.spawn(command)
184             puts pid.to_s
185           end
186           
187           ::Process.waitpid(opid, 0)
188           w.close
189           pid = r.gets.chomp
190         else
191           # single fork self-daemonizing processes
192           # we want to wait for them to finish
193           pid = self.spawn(command)
194           ::Process.waitpid(pid, 0)
195         end
196         
197         if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
198           File.open(default_pid_file, 'w') do |f|
199             f.write pid
200           end
201           
202           @tracking_pid = true
203           @pid_file = default_pid_file
204         end
205       elsif command.kind_of?(Proc)
206         # lambda command
207         command.call
208       else
209         raise NotImplementedError
210       end
211     end
212     
213     def default_pid_file
214       File.join(God.pid_file_directory, "#{self.name}.pid")
215     end
216   end