dante 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/dante/runner.rb CHANGED
@@ -17,6 +17,10 @@ This is a utility for setting up a binary executable for a service.
17
17
  module Dante
18
18
  class Runner
19
19
  MAX_START_TRIES = 5
20
+ # Seconds a new daemon must stay up before it counts as started.
21
+ # ceiling: a daemon that dies later than this is still reported as started; a
22
+ # readiness signal from the startup block would close that.
23
+ START_GRACE = 1
20
24
 
21
25
  attr_accessor :options, :name, :description, :verify_options_hook
22
26
 
@@ -59,16 +63,7 @@ module Dante
59
63
 
60
64
  # If a username, uid, groupname, or gid is passed,
61
65
  # drop privileges accordingly.
62
-
63
- if options[:group]
64
- gid = options[:group].is_a?(Integer) ? options[:group] : Etc.getgrnam(options[:group]).gid
65
- Process::GID.change_privilege(gid)
66
- end
67
-
68
- if options[:user]
69
- uid = options[:user].is_a?(Integer) ? options[:user] : Etc.getpwnam(options[:user]).uid
70
- Process::UID.change_privilege(uid)
71
- end
66
+ switch_user(options[:user], options[:group]) if options[:user] || options[:group]
72
67
 
73
68
  @startup_command = block if block_given?
74
69
  options[:daemonize] ? daemonize : start
@@ -82,19 +77,48 @@ module Dante
82
77
  options[:log_path] = "/var/log/#{@name}.log"
83
78
  end
84
79
 
85
- # Start process
80
+ # Start process: a session leader forks the daemon, then exits with success only
81
+ # if the daemon is still running START_GRACE seconds later. As the daemon's parent
82
+ # it sees the daemon exit at once, where the pid file cannot tell.
86
83
  pid = fork do
87
- exit if fork
88
- Process.setsid
89
- exit if fork
90
- store_pid(Process.pid)
91
- File.umask 0000
92
- redirect_output!
93
- start
84
+ started = false
85
+ begin
86
+ Process.setsid
87
+ daemon = fork do
88
+ # Handle INT and TERM before the pid file tells the caller we have started
89
+ trap_signals
90
+ store_pid(Process.pid)
91
+ redirect_output!
92
+ start
93
+ end
94
+ started = daemon_survives?(daemon)
95
+ remove_pid_file(daemon) unless started # the pid of a daemon that is gone
96
+ rescue Exception => e
97
+ warn "#{e.class}: #{e.message}"
98
+ ensure
99
+ # Like Process.daemon, leave without running the at_exit handlers inherited
100
+ # from the caller: they are the caller's, and one that exits or raises would
101
+ # replace this exit status.
102
+ exit!(started)
103
+ end
104
+ end
105
+ waited = false
106
+ started = begin
107
+ survived = Process.waitpid2(pid)[1].success?
108
+ waited = true
109
+ # Ensure process is running
110
+ survived && until_true(MAX_START_TRIES) { self.daemon_running? }
111
+ rescue Errno::ECHILD
112
+ # SIGCHLD is ignored, so the session leader's status is lost. It has exited, though,
113
+ # removing the pid file of a daemon that died, so one look at the pid file tells.
114
+ waited = true
115
+ self.daemon_running?
116
+ ensure
117
+ # Interrupted while the session leader runs: reap it in the background, or it
118
+ # lingers as a zombie.
119
+ Process.detach(pid) unless waited
94
120
  end
95
- Process.waitpid pid
96
- # Ensure process is running
97
- if until_true(MAX_START_TRIES) { self.daemon_running? }
121
+ if started
98
122
  log "Daemon has started successfully"
99
123
  true
100
124
  else # Failed to start
@@ -110,15 +134,7 @@ module Dante
110
134
  redirect_output!
111
135
  end
112
136
 
113
- trap("INT") {
114
- interrupt
115
- exit
116
- }
117
-
118
- trap("TERM"){
119
- log "Trying to stop #{@name}..."
120
- exit
121
- }
137
+ trap_signals
122
138
 
123
139
  @startup_command.call(self.options) if @startup_command
124
140
  end
@@ -126,8 +142,18 @@ module Dante
126
142
  # Stops a daemonized process
127
143
  def stop(kill_arg=nil)
128
144
  if self.daemon_running?
129
- kill_pid(kill_arg || options[:kill])
130
- until_true(MAX_START_TRIES) { self.daemon_stopped? }
145
+ if wait_for_stop(kill_pid(kill_arg || options[:kill]))
146
+ FileUtils.rm options[:pid_path] # Only kill if we stopped the daemon
147
+ log "Daemonized process killed after term."
148
+ else
149
+ log "Failed to kill daemonized process"
150
+ if options[:force]
151
+ if wait_for_stop(kill_pid(kill_arg || options[:kill], 'KILL'))
152
+ FileUtils.rm options[:pid_path] # Only kill if we stopped the daemon
153
+ log "Daemonized process killed after kill."
154
+ end
155
+ end
156
+ end
131
157
  else # not running
132
158
  log "No #{@name} processes are running"
133
159
  false
@@ -157,7 +183,10 @@ module Dante
157
183
  # self.daemon_running?
158
184
  def daemon_running?
159
185
  return false unless File.exist?(options[:pid_path])
160
- Process.kill 0, File.read(options[:pid_path]).to_i
186
+ return false unless pid = read_pid(options[:pid_path])
187
+ Process.kill 0, pid
188
+ true
189
+ rescue Errno::EPERM # the process exists but belongs to another user
161
190
  true
162
191
  rescue Errno::ESRCH
163
192
  false
@@ -170,7 +199,7 @@ module Dante
170
199
  OptionParser.new do |opts|
171
200
  opts.summary_width = 25
172
201
  opts.banner = [headline, "\n\n",
173
- "Usage: #{@name} [-p port] [-P file] [-d] [-k]\n",
202
+ "Usage: #{@name} [-p port] [-P file] [-d] [-k] [-f]\n",
174
203
  " #{@name} --help\n"].compact.join("")
175
204
  opts.separator ""
176
205
 
@@ -202,33 +231,127 @@ module Dante
202
231
  options[:group] = group
203
232
  end
204
233
 
234
+ opts.on("-f", "--force", String, "Force kill if SIGTERM fails") do |v|
235
+ options[:force] = true
236
+ end
237
+
205
238
  opts.on_tail("-?", "--help", "Display this usage information.") do
206
239
  puts "#{opts}\n"
207
240
  exit
208
241
  end
209
-
242
+
210
243
  # Load options specified through 'with_options'
211
244
  instance_exec(opts, &@with_options) if @with_options
212
245
  end.parse!
213
246
  options
214
247
  end
215
248
 
249
+ # Switches the process to user (a name or uid) and group (a name or gid). When running
250
+ # as root the group defaults to the user's primary group, and the supplementary groups
251
+ # (which changing the uid and gid leaves as they were) are set too: to the user's own
252
+ # groups, or to just the process's group for a group alone or a uid with no passwd
253
+ # entry. Otherwise only what was given changes. Everything is looked up before
254
+ # anything is changed.
255
+ def switch_user(user, group)
256
+ root = Process.euid == 0
257
+ passwd = if user.is_a?(Integer)
258
+ begin
259
+ Etc.getpwuid(user)
260
+ rescue ArgumentError # a uid with no passwd entry
261
+ nil
262
+ end
263
+ elsif user
264
+ Etc.getpwnam(user)
265
+ end
266
+ uid = passwd ? passwd.uid : user
267
+ gid = if group
268
+ group.is_a?(Integer) ? group : Etc.getgrnam(group).gid
269
+ elsif passwd && root
270
+ passwd.gid
271
+ end
272
+
273
+ if root
274
+ if passwd
275
+ Process.initgroups(passwd.name, gid)
276
+ else
277
+ Process.groups = [gid || Process.egid]
278
+ end
279
+ end
280
+ Process::GID.change_privilege(gid) if gid
281
+ Process::UID.change_privilege(uid) if uid
282
+ end
283
+
284
+ def trap_signals
285
+ trap("INT") {
286
+ interrupt
287
+ exit
288
+ }
289
+
290
+ trap("TERM"){
291
+ log "Trying to stop #{@name}..."
292
+ exit
293
+ }
294
+ end
295
+
216
296
  def store_pid(pid)
217
297
  FileUtils.mkdir_p(File.dirname(options[:pid_path]))
218
298
  File.open(options[:pid_path], 'w'){|f| f.write("#{pid}\n")}
219
299
  end
220
300
 
221
- def kill_pid(k='*')
222
- Dir[options[:pid_path]].each do |f|
301
+ # Whether daemon, a child of this process, is still running START_GRACE seconds after
302
+ # it was forked. When the caller's SIGCHLD handling reaps children itself (SIGCHLD
303
+ # ignored, or a handler that waits for them), waitpid cannot see the daemon, and
304
+ # signal 0 at the end of the grace period tells instead.
305
+ def daemon_survives?(daemon)
306
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + START_GRACE
307
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
308
+ return false if Process.waitpid(daemon, Process::WNOHANG)
309
+ sleep 0.1
310
+ end
311
+ true
312
+ rescue Errno::ECHILD
313
+ sleep [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
314
+ begin
315
+ Process.kill(0, daemon)
316
+ true
317
+ rescue Errno::ESRCH
318
+ false
319
+ end
320
+ end
321
+
322
+ # Removes the pid file if it still holds pid (another start may have replaced it).
323
+ def remove_pid_file(pid)
324
+ File.delete(options[:pid_path]) if read_pid(options[:pid_path]) == pid
325
+ rescue Errno::ENOENT
326
+ end
327
+
328
+ # The pid in pid_file, or nil when it holds none: a pid file left empty by a crash
329
+ # between creating and writing it names no process (and pid 0 or a negative pid would
330
+ # signal a whole process group), nor does a number too large for a pid_t.
331
+ def read_pid(pid_file)
332
+ pid = File.read(pid_file).to_i
333
+ pid if pid > 0 && pid < 2**31
334
+ end
335
+
336
+ # Sends sig to the daemons, and returns whether it reached any of them.
337
+ def kill_pid(k='*', sig='TERM')
338
+ Dir[options[:pid_path]].count do |f|
223
339
  begin
224
- pid = IO.read(f).chomp.to_i
225
- FileUtils.rm f
226
- Process.kill('TERM', pid)
227
- log "Stopped PID: #{pid} at #{f}"
340
+ next false unless pid = read_pid(f)
341
+ Process.kill(sig, pid)
342
+ log "Sent SIG#{sig} to PID: #{pid} at #{f}"
343
+ true
228
344
  rescue => e
229
345
  log "Failed to stop! #{k}: #{e}"
346
+ false
230
347
  end
231
- end
348
+ end > 0
349
+ end
350
+
351
+ # Waits for the daemon to stop once a signal reached it. When none did (the daemon
352
+ # belongs to another user), it cannot stop, so this only checks once.
353
+ def wait_for_stop(signalled)
354
+ signalled ? until_true(MAX_START_TRIES) { self.daemon_stopped? } : self.daemon_stopped?
232
355
  end
233
356
 
234
357
  # Redirect output based on log settings (reopens stdout/stderr to specified logfile)
@@ -242,20 +365,27 @@ module Dante
242
365
  # Set permissions on the log file
243
366
  File.chmod(0644, log_path)
244
367
  # Reopen $stdout (NOT +STDOUT+) to start writing to the log file
245
- $stdout.reopen(log_path, 'a')
368
+ reopen_stream($stdout, log_path, 'a')
246
369
  # Redirect $stderr to $stdout
247
370
  $stderr.reopen $stdout
248
371
  $stdout.sync = true
249
372
  else # redirect to /dev/null
250
373
  # We're not bothering to sync if we're dumping to /dev/null
251
374
  # because /dev/null doesn't care about buffered output
252
- $stdin.reopen '/dev/null'
253
- $stdout.reopen '/dev/null', 'a'
375
+ reopen_stream($stdin, '/dev/null', 'r')
376
+ reopen_stream($stdout, '/dev/null', 'a')
254
377
  $stderr.reopen $stdout
255
378
  end
256
379
  log_path = options[:log_path] ? options[:log_path] : '/dev/null'
257
380
  end
258
381
 
382
+ # Reopens stream onto path, opened with mode. Reopening onto an open File rather
383
+ # than a path works whatever mode the stream itself was opened with (a socket, for
384
+ # one, when run under Spring or Zeus).
385
+ def reopen_stream(stream, path, mode)
386
+ File.open(path, mode) { |file| stream.reopen(file) }
387
+ end
388
+
259
389
  # Runs until the block condition is met or the timeout_seconds is exceeded
260
390
  # until_true(10) { ...return_condition... }
261
391
  def until_true(timeout_seconds, interval=1, &block)
data/lib/dante/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dante
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/test/dante_test.rb CHANGED
@@ -2,7 +2,7 @@ require File.expand_path('../test_helper', __FILE__)
2
2
 
3
3
  describe "dante module" do
4
4
  before do
5
- @process = TestingProcess.new('a')
5
+ @process = TestingProcess.new('a', @tmpdir)
6
6
  end
7
7
 
8
8
  it "can run jobs using #run method" do
@@ -10,7 +10,7 @@ describe "dante module" do
10
10
  Dante.run('test-process') { @process.run_a! }
11
11
  end
12
12
  @output = File.read(@process.tmp_path)
13
- assert_match /Started/, @output
13
+ assert_match(/Started/, @output)
14
14
  end
15
15
 
16
- end
16
+ end