File: process.rb

package info (click to toggle)
ruby-god 0.12.1-1
  • links: PTS
  • area: main
  • in suites: wheezy
  • size: 752 kB
  • sloc: ruby: 5,913; ansic: 217; makefile: 3
file content (364 lines) | stat: -rw-r--r-- 10,225 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
module God
  class Process
    WRITES_PID = [:start, :restart]

    attr_accessor :name, :uid, :gid, :log, :log_cmd, :err_log, :err_log_cmd,
                  :start, :stop, :restart, :unix_socket, :chroot, :env, :dir,
                  :stop_timeout, :stop_signal, :umask

    def initialize
      self.log = '/dev/null'

      @pid_file = nil
      @tracking_pid = true
      @user_log = false
      @pid = nil
      @unix_socket = nil
      @log_cmd = nil
      @stop_timeout = God::STOP_TIMEOUT_DEFAULT
      @stop_signal = God::STOP_SIGNAL_DEFAULT
    end

    def alive?
      if self.pid
        System::Process.new(self.pid).exists?
      else
        false
      end
    end

    def file_writable?(file)
      pid = fork do
        begin
          uid_num = Etc.getpwnam(self.uid).uid if self.uid
          gid_num = Etc.getgrnam(self.gid).gid if self.gid

          ::Dir.chroot(self.chroot) if self.chroot
          ::Process.groups = [gid_num] if self.gid
          ::Process::Sys.setgid(gid_num) if self.gid
          ::Process::Sys.setuid(uid_num) if self.uid
        rescue ArgumentError, Errno::EPERM, Errno::ENOENT
          exit(1)
        end

        File.writable?(file_in_chroot(file)) ? exit(0) : exit(1)
      end

      wpid, status = ::Process.waitpid2(pid)
      status.exitstatus == 0 ? true : false
    end

    def valid?
      # determine if we're tracking pid or not
      self.pid_file

      valid = true

      # a start command must be specified
      if self.start.nil?
        valid = false
        applog(self, :error, "No start command was specified")
      end

      # uid must exist if specified
      if self.uid
        begin
          Etc.getpwnam(self.uid)
        rescue ArgumentError
          valid = false
          applog(self, :error, "UID for '#{self.uid}' does not exist")
        end
      end

      # gid must exist if specified
      if self.gid
        begin
          Etc.getgrnam(self.gid)
        rescue ArgumentError
          valid = false
          applog(self, :error, "GID for '#{self.gid}' does not exist")
        end
      end

      # dir must exist and be a directory if specified
      if self.dir
        if !File.exist?(self.dir)
          valid = false
          applog(self, :error, "Specified directory '#{self.dir}' does not exist")
        elsif !File.directory?(self.dir)
          valid = false
          applog(self, :error, "Specified directory '#{self.dir}' is not a directory")
        end
      end

      # pid dir must exist if specified
      if !@tracking_pid && !File.exist?(File.dirname(self.pid_file))
        valid = false
        applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' does not exist")
      end

      # pid dir must be writable if specified
      if !@tracking_pid && File.exist?(File.dirname(self.pid_file)) && !file_writable?(File.dirname(self.pid_file))
        valid = false
        applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' is not writable by #{self.uid || Etc.getlogin}")
      end

      # log dir must exist
      if !File.exist?(File.dirname(self.log))
        valid = false
        applog(self, :error, "Log directory '#{File.dirname(self.log)}' does not exist")
      end

      # log file or dir must be writable
      if File.exist?(self.log)
        unless file_writable?(self.log)
          valid = false
          applog(self, :error, "Log file '#{self.log}' exists but is not writable by #{self.uid || Etc.getlogin}")
        end
      else
        unless file_writable?(File.dirname(self.log))
          valid = false
          applog(self, :error, "Log directory '#{File.dirname(self.log)}' is not writable by #{self.uid || Etc.getlogin}")
        end
      end

      # chroot directory must exist and have /dev/null in it
      if self.chroot
        if !File.directory?(self.chroot)
          valid = false
          applog(self, :error, "CHROOT directory '#{self.chroot}' does not exist")
        end

        if !File.exist?(File.join(self.chroot, '/dev/null'))
          valid = false
          applog(self, :error, "CHROOT directory '#{self.chroot}' does not contain '/dev/null'")
        end
      end

      valid
    end

    # DON'T USE THIS INTERNALLY. Use the instance variable. -- Kev
    # No really, trust me. Use the instance variable.
    def pid_file=(value)
      # if value is nil, do the right thing
      if value
        @tracking_pid = false
      else
        @tracking_pid = true
      end

      @pid_file = value
    end

    def pid_file
      @pid_file ||= default_pid_file
    end

    # Fetch the PID from pid_file. If the pid_file does not
    # exist, then use the PID from the last time it was read.
    # If it has never been read, then return nil.
    #
    # Returns Integer(pid) or nil
    def pid
      contents = File.read(self.pid_file).strip rescue ''
      real_pid = contents =~ /^\d+$/ ? contents.to_i : nil

      if real_pid
        @pid = real_pid
        real_pid
      else
        @pid
      end
    end

    # Send the given signal to this process.
    #
    # Returns nothing
    def signal(sig)
      sig = sig.to_i if sig.to_i != 0
      applog(self, :info, "#{self.name} sending signal '#{sig}' to pid #{self.pid}")
      ::Process.kill(sig, self.pid) rescue nil
    end

    def start!
      call_action(:start)
    end

    def stop!
      call_action(:stop)
    end

    def restart!
      call_action(:restart)
    end

    def default_pid_file
      File.join(God.pid_file_directory, "#{self.name}.pid")
    end

    def call_action(action)
      command = send(action)

      if action == :stop && command.nil?
        pid = self.pid
        name = self.name
        command = lambda do
          applog(self, :info, "#{self.name} stop: default lambda killer")

          ::Process.kill(@stop_signal, pid) rescue nil
          applog(self, :info, "#{self.name} sent SIG#{@stop_signal}")

          # Poll to see if it's dead
          @stop_timeout.times do
            begin
              ::Process.kill(0, pid)
            rescue Errno::ESRCH
              # It died. Good.
              applog(self, :info, "#{self.name} process stopped")
              return
            end

            sleep 1
          end

          ::Process.kill('KILL', pid) rescue nil
          applog(self, :warn, "#{self.name} still alive after #{@stop_timeout}s; sent SIGKILL")
        end
      end

      if command.kind_of?(String)
        pid = nil

        if [:start, :restart].include?(action) && @tracking_pid
          # double fork god-daemonized processes
          # we don't want to wait for them to finish
          r, w = IO.pipe
          begin
            opid = fork do
              STDOUT.reopen(w)
              r.close
              pid = self.spawn(command)
              puts pid.to_s # send pid back to forker
            end

            ::Process.waitpid(opid, 0)
            w.close
            pid = r.gets.chomp
          ensure
            # make sure the file descriptors get closed no matter what
            r.close rescue nil
            w.close rescue nil
          end
        else
          # single fork self-daemonizing processes
          # we want to wait for them to finish
          pid = self.spawn(command)
          status = ::Process.waitpid2(pid, 0)
          exit_code = status[1] >> 8

          if exit_code != 0
            applog(self, :warn, "#{self.name} #{action} command exited with non-zero code = #{exit_code}")
          end

          ensure_stop if action == :stop
        end

        if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
          File.open(default_pid_file, 'w') do |f|
            f.write pid
          end

          @tracking_pid = true
          @pid_file = default_pid_file
        end
      elsif command.kind_of?(Proc)
        # lambda command
        command.call
      else
        raise NotImplementedError
      end
    end

    # Fork/exec the given command, returns immediately
    #   +command+ is the String containing the shell command
    #
    # Returns nothing
    def spawn(command)
      fork do
        File.umask self.umask if self.umask
        uid_num = Etc.getpwnam(self.uid).uid if self.uid
        gid_num = Etc.getgrnam(self.gid).gid if self.gid

        ::Dir.chroot(self.chroot) if self.chroot
        ::Process.setsid
        ::Process.groups = [gid_num] if self.gid
        ::Process::Sys.setgid(gid_num) if self.gid
        ::Process::Sys.setuid(uid_num) if self.uid
        self.dir ||= '/'
        Dir.chdir self.dir
        $0 = command
        STDIN.reopen "/dev/null"
        if self.log_cmd
          STDOUT.reopen IO.popen(self.log_cmd, "a")
        else
          STDOUT.reopen file_in_chroot(self.log), "a"
        end
        if err_log_cmd
          STDERR.reopen IO.popen(err_log_cmd, "a")
        elsif err_log && (log_cmd || err_log != log)
          STDERR.reopen file_in_chroot(err_log), "a"
        else
          STDERR.reopen STDOUT
        end

        # close any other file descriptors
        3.upto(256){|fd| IO::new(fd).close rescue nil}

        if self.env && self.env.is_a?(Hash)
          self.env.each do |(key, value)|
            ENV[key] = value.to_s
          end
        end

        exec command unless command.empty?
      end
    end

    # Ensure that a stop command actually stops the process. Force kill
    # if necessary.
    #
    # Returns nothing
    def ensure_stop
      applog(self, :warn, "#{self.name} ensuring stop...")

      unless self.pid
        applog(self, :warn, "#{self.name} stop called but pid is uknown")
        return
      end

      # Poll to see if it's dead
      @stop_timeout.times do
        begin
          ::Process.kill(0, self.pid)
        rescue Errno::ESRCH
          # It died. Good.
          return
        end

        sleep 1
      end

      # last resort
      ::Process.kill('KILL', self.pid) rescue nil
      applog(self, :warn, "#{self.name} still alive after #{@stop_timeout}s; sent SIGKILL")
    end

    private
    def file_in_chroot(file)
      return file unless self.chroot

      file.gsub(/^#{Regexp.escape(File.expand_path(self.chroot))}/, '')
    end
  end
end