daemon_controller 2.0.0 → 3.0.1
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.
- checksums.yaml +4 -4
- data/README.md +77 -75
- data/Rakefile +9 -11
- data/lib/daemon_controller/version.rb +2 -2
- data/lib/daemon_controller.rb +309 -449
- data/spec/daemon_controller_spec.rb +341 -227
- data/spec/echo_server.rb +9 -1
- data/spec/test_helper.rb +67 -20
- data/spec/unresponsive_daemon.rb +6 -12
- metadata +3 -4
- data/spec/run_echo_server +0 -9
data/lib/daemon_controller.rb
CHANGED
|
@@ -63,172 +63,39 @@ class DaemonController
|
|
|
63
63
|
class ConnectError < Error
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def initialize(output)
|
|
71
|
-
super()
|
|
72
|
-
@output = output
|
|
73
|
-
end
|
|
74
|
-
end
|
|
66
|
+
InternalCommandOkResult = Struct.new(:pid, :output)
|
|
67
|
+
InternalCommandErrorResult = Struct.new(:pid, :output, :exit_status)
|
|
68
|
+
InternalCommandTimeoutResult = Struct.new(:pid, :output)
|
|
75
69
|
|
|
76
70
|
# Create a new DaemonController object.
|
|
77
71
|
#
|
|
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
|
-
# - [:tcp, host_name, port]
|
|
106
|
-
# - [:unix, filename]
|
|
107
|
-
#
|
|
108
|
-
# The value may also be a Proc, which returns an expression that evaluates to
|
|
109
|
-
# true (indicating that the daemon can be connected to) or false (failure).
|
|
110
|
-
# If the Proc raises Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::ETIMEDOUT
|
|
111
|
-
# Errno::ECONNRESET, Errno::EINVAL or Errno::EADDRNOTAVAIL then that also
|
|
112
|
-
# means that the daemon cannot be connected to.
|
|
113
|
-
# <b>NOTE:</b> if the ping command returns an object which responds to
|
|
114
|
-
# <tt>#close</tt>, then that method will be called on it.
|
|
115
|
-
# This makes it possible to specify a ping command such as
|
|
116
|
-
# <tt>lambda { TCPSocket.new('localhost', 1234) }</tt>, without having to worry
|
|
117
|
-
# about closing it afterwards.
|
|
118
|
-
# Any exceptions raised by #close are ignored.
|
|
119
|
-
#
|
|
120
|
-
# [:pid_file]
|
|
121
|
-
# The PID file that the daemon will write to. Used to check whether the daemon
|
|
122
|
-
# is running.
|
|
123
|
-
#
|
|
124
|
-
# [:lock_file]
|
|
125
|
-
# The lock file to use for serializing concurrent daemon management operations.
|
|
126
|
-
# Defaults to "(filename of PID file).lock".
|
|
127
|
-
#
|
|
128
|
-
# [:log_file]
|
|
129
|
-
# The log file that the daemon will write to. It will be consulted to see
|
|
130
|
-
# whether the daemon has printed any error messages during startup.
|
|
131
|
-
#
|
|
132
|
-
# === Optional options
|
|
133
|
-
# [:stop_command]
|
|
134
|
-
# A command to stop the daemon with, e.g. "/etc/rc.d/nginx stop". If no stop
|
|
135
|
-
# command is given (i.e. +nil+), then DaemonController will stop the daemon
|
|
136
|
-
# by killing the PID written in the PID file.
|
|
137
|
-
#
|
|
138
|
-
# The default value is +nil+.
|
|
139
|
-
#
|
|
140
|
-
# [:restart_command]
|
|
141
|
-
# A command to restart the daemon with, e.g. "/etc/rc.d/nginx restart". If
|
|
142
|
-
# no restart command is given (i.e. +nil+), then DaemonController will
|
|
143
|
-
# restart the daemon by calling #stop and #start.
|
|
144
|
-
#
|
|
145
|
-
# The default value is +nil+.
|
|
146
|
-
#
|
|
147
|
-
# [:before_start]
|
|
148
|
-
# This may be a Proc. It will be called just before running the start command.
|
|
149
|
-
# The before_start proc is not subject to the start timeout.
|
|
150
|
-
#
|
|
151
|
-
# [:start_timeout]
|
|
152
|
-
# The maximum amount of time, in seconds, that #start may take to start
|
|
153
|
-
# the daemon. Since #start also waits until the daemon can be connected to,
|
|
154
|
-
# that wait time is counted as well. If the daemon does not start in time,
|
|
155
|
-
# then #start will raise an exception.
|
|
156
|
-
#
|
|
157
|
-
# The default value is 15.
|
|
158
|
-
#
|
|
159
|
-
# [:stop_timeout]
|
|
160
|
-
# The maximum amount of time, in seconds, that #stop may take to stop
|
|
161
|
-
# the daemon. Since #stop also waits until the daemon is no longer running,
|
|
162
|
-
# that wait time is counted as well. If the daemon does not stop in time,
|
|
163
|
-
# then #stop will raise an exception.
|
|
164
|
-
#
|
|
165
|
-
# The default value is 15.
|
|
166
|
-
#
|
|
167
|
-
# [:log_file_activity_timeout]
|
|
168
|
-
# Once a daemon has gone into the background, it will become difficult to
|
|
169
|
-
# know for certain whether it is still initializing or whether it has
|
|
170
|
-
# failed and exited, until it has written its PID file. Suppose that it
|
|
171
|
-
# failed with an error after daemonizing but before it has written its PID file;
|
|
172
|
-
# not many system administrators want to wait 15 seconds (the default start
|
|
173
|
-
# timeout) to be notified of whether the daemon has terminated with an error.
|
|
174
|
-
#
|
|
175
|
-
# An alternative way to check whether the daemon has terminated with an error,
|
|
176
|
-
# is by checking whether its log file has been recently updated. If, after the
|
|
177
|
-
# daemon has started, the log file hasn't been updated for the amount of seconds
|
|
178
|
-
# given by the :log_file_activity_timeout option, then the daemon is assumed to
|
|
179
|
-
# have terminated with an error.
|
|
180
|
-
#
|
|
181
|
-
# The default value is 7.
|
|
182
|
-
#
|
|
183
|
-
# [:dont_stop_if_pid_file_invalid]
|
|
184
|
-
# If the :stop_command option is given, then normally daemon_controller will
|
|
185
|
-
# always execute this command upon calling #stop. But if :dont_stop_if_pid_file_invalid
|
|
186
|
-
# is given, then daemon_controller will not do that if the PID file does not contain
|
|
187
|
-
# a valid number.
|
|
188
|
-
#
|
|
189
|
-
# The default is false.
|
|
190
|
-
#
|
|
191
|
-
# [:daemonize_for_me]
|
|
192
|
-
# Normally daemon_controller will wait until the daemon has daemonized into the
|
|
193
|
-
# background, in order to capture any errors that it may print on stdout or
|
|
194
|
-
# stderr before daemonizing. However, if the daemon doesn't support daemonization
|
|
195
|
-
# for some reason, then setting this option to true will cause daemon_controller
|
|
196
|
-
# to do the daemonization for the daemon.
|
|
197
|
-
#
|
|
198
|
-
# The default is false.
|
|
199
|
-
#
|
|
200
|
-
# [:keep_ios]
|
|
201
|
-
# Upon spawning the daemon, daemon_controller will normally close all file
|
|
202
|
-
# descriptors except stdin, stdout and stderr. However if there are any file
|
|
203
|
-
# descriptors you want to keep open, specify the IO objects here. This must be
|
|
204
|
-
# an array of IO objects.
|
|
205
|
-
#
|
|
206
|
-
# [:env]
|
|
207
|
-
# This must be a Hash. The hash will contain the environment variables available
|
|
208
|
-
# to be made available to the daemon. Hash keys must be strings, not symbols.
|
|
209
|
-
def initialize(options)
|
|
210
|
-
[:identifier, :start_command, :ping_command, :pid_file, :log_file].each do |option|
|
|
211
|
-
if !options.has_key?(option)
|
|
212
|
-
raise ArgumentError, "The ':#{option}' option is mandatory"
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
@identifier = options[:identifier]
|
|
216
|
-
@start_command = options[:start_command]
|
|
217
|
-
@stop_command = options[:stop_command]
|
|
218
|
-
@ping_command = options[:ping_command]
|
|
219
|
-
@restart_command = options[:restart_command]
|
|
220
|
-
@ping_interval = options[:ping_interval] || 0.1
|
|
221
|
-
@pid_file = options[:pid_file]
|
|
222
|
-
@log_file = options[:log_file]
|
|
223
|
-
@before_start = options[:before_start]
|
|
224
|
-
@start_timeout = options[:start_timeout] || 15
|
|
225
|
-
@stop_timeout = options[:stop_timeout] || 15
|
|
226
|
-
@log_file_activity_timeout = options[:log_file_activity_timeout] || 7
|
|
227
|
-
@dont_stop_if_pid_file_invalid = options[:dont_stop_if_pid_file_invalid]
|
|
228
|
-
@daemonize_for_me = options[:daemonize_for_me]
|
|
229
|
-
@keep_ios = options[:keep_ios] || []
|
|
230
|
-
@lock_file = determine_lock_file(options, @identifier, @pid_file)
|
|
231
|
-
@env = options[:env] || {}
|
|
72
|
+
# See doc/OPTIONS.md for options docs.
|
|
73
|
+
def initialize(identifier:, start_command:, ping_command:, pid_file:, log_file:,
|
|
74
|
+
lock_file: nil, stop_command: nil, restart_command: nil, before_start: nil,
|
|
75
|
+
start_timeout: 30, start_abort_timeout: 10, stop_timeout: 30,
|
|
76
|
+
log_file_activity_timeout: 10, ping_interval: 0.1, stop_graceful_signal: "TERM", dont_stop_if_pid_file_invalid: false,
|
|
77
|
+
daemonize_for_me: false, keep_ios: nil, env: {}, logger: nil)
|
|
78
|
+
@identifier = identifier
|
|
79
|
+
@start_command = start_command
|
|
80
|
+
@ping_command = ping_command
|
|
81
|
+
@pid_file = pid_file
|
|
82
|
+
@log_file = log_file
|
|
83
|
+
|
|
84
|
+
@lock_file = determine_lock_file(lock_file, identifier, pid_file)
|
|
85
|
+
@stop_command = stop_command
|
|
86
|
+
@restart_command = restart_command
|
|
87
|
+
@before_start = before_start
|
|
88
|
+
@start_timeout = start_timeout
|
|
89
|
+
@start_abort_timeout = start_abort_timeout
|
|
90
|
+
@stop_timeout = stop_timeout
|
|
91
|
+
@log_file_activity_timeout = log_file_activity_timeout
|
|
92
|
+
@ping_interval = ping_interval
|
|
93
|
+
@stop_graceful_signal = stop_graceful_signal
|
|
94
|
+
@dont_stop_if_pid_file_invalid = dont_stop_if_pid_file_invalid
|
|
95
|
+
@daemonize_for_me = daemonize_for_me
|
|
96
|
+
@keep_ios = keep_ios
|
|
97
|
+
@env = env
|
|
98
|
+
@logger = logger
|
|
232
99
|
end
|
|
233
100
|
|
|
234
101
|
# Start the daemon and wait until it can be pinged.
|
|
@@ -273,7 +140,7 @@ class DaemonController
|
|
|
273
140
|
end
|
|
274
141
|
if connection.nil?
|
|
275
142
|
@lock_file.exclusive_lock do
|
|
276
|
-
|
|
143
|
+
unless daemon_is_running?
|
|
277
144
|
start_without_locking
|
|
278
145
|
end
|
|
279
146
|
connect_exception = nil
|
|
@@ -310,15 +177,17 @@ class DaemonController
|
|
|
310
177
|
# - StopTimeout - the daemon didn't stop in time.
|
|
311
178
|
def stop
|
|
312
179
|
@lock_file.exclusive_lock do
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
!daemon_is_running?
|
|
180
|
+
timeoutable(@stop_timeout) do
|
|
181
|
+
allow_timeout do
|
|
182
|
+
kill_daemon
|
|
183
|
+
wait_until { !daemon_is_running? }
|
|
317
184
|
end
|
|
318
185
|
end
|
|
319
|
-
rescue Timeout::Error
|
|
320
|
-
raise StopTimeout, "Daemon '#{@identifier}' did not exit in time"
|
|
321
186
|
end
|
|
187
|
+
rescue Timeout::Error
|
|
188
|
+
kill_daemon_with_signal(force: true)
|
|
189
|
+
wait_until { !daemon_is_running? }
|
|
190
|
+
raise StopTimeout, "Daemon '#{@identifier}' did not exit in time (force killed)"
|
|
322
191
|
end
|
|
323
192
|
|
|
324
193
|
# Restarts the daemon. Uses the restart_command if provided, otherwise
|
|
@@ -367,66 +236,86 @@ class DaemonController
|
|
|
367
236
|
private
|
|
368
237
|
|
|
369
238
|
def start_without_locking
|
|
370
|
-
if daemon_is_running?
|
|
371
|
-
|
|
372
|
-
end
|
|
239
|
+
raise AlreadyStarted, "Daemon '#{@identifier}' is already started" if daemon_is_running?
|
|
240
|
+
|
|
373
241
|
save_log_file_information
|
|
374
242
|
delete_pid_file
|
|
243
|
+
spawn_result = nil
|
|
244
|
+
|
|
375
245
|
begin
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
246
|
+
_, remaining_time = timeoutable(@start_timeout) do
|
|
247
|
+
allow_timeout { before_start }
|
|
248
|
+
spawn_result = allow_timeout { spawn_daemon }
|
|
249
|
+
daemon_spawned
|
|
380
250
|
record_activity
|
|
381
251
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
252
|
+
if spawn_result.is_a?(InternalCommandOkResult)
|
|
253
|
+
allow_timeout do
|
|
254
|
+
# We wait until the PID file is available and until
|
|
255
|
+
# the daemon responds to pings, but we wait no longer
|
|
256
|
+
# than @start_timeout seconds in total (including daemon
|
|
257
|
+
# spawn time).
|
|
258
|
+
# Furthermore, if the log file hasn't changed for
|
|
259
|
+
# @log_file_activity_timeout seconds, and the PID file
|
|
260
|
+
# still isn't available or the daemon still doesn't
|
|
261
|
+
# respond to pings, then assume that the daemon has
|
|
262
|
+
# terminated with an error.
|
|
263
|
+
wait_until do
|
|
264
|
+
if log_file_has_changed?
|
|
265
|
+
record_activity
|
|
266
|
+
elsif no_activity?(@log_file_activity_timeout)
|
|
267
|
+
raise Timeout::Error, "Log file inactivity"
|
|
268
|
+
end
|
|
269
|
+
pid_file_available?
|
|
270
|
+
end
|
|
271
|
+
wait_until(sleep_interval: @ping_interval) do
|
|
272
|
+
if log_file_has_changed?
|
|
273
|
+
record_activity
|
|
274
|
+
elsif no_activity?(@log_file_activity_timeout)
|
|
275
|
+
raise Timeout::Error, "Log file inactivity"
|
|
276
|
+
end
|
|
277
|
+
run_ping_command || !daemon_is_running?
|
|
278
|
+
end
|
|
404
279
|
end
|
|
405
|
-
run_ping_command || !daemon_is_running?
|
|
406
280
|
end
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
result = started
|
|
410
|
-
rescue DaemonizationTimeout, Timeout::Error => e
|
|
411
|
-
start_timed_out
|
|
412
|
-
if pid_file_available?
|
|
413
|
-
kill_daemon_with_signal(true)
|
|
281
|
+
|
|
282
|
+
spawn_result
|
|
414
283
|
end
|
|
415
|
-
|
|
416
|
-
|
|
284
|
+
rescue Timeout::Error
|
|
285
|
+
# If we got here then it means either the #before_start timed out (= no PID),
|
|
286
|
+
# or the code after #spawn_daemon timed out (already daemonized, so use PID file).
|
|
287
|
+
# #spawn_daemon itself won't trigger Timeout:Error because that's handled as
|
|
288
|
+
# InternalCommandTimeoutResult.
|
|
289
|
+
pid = spawn_result ? read_pid_file : nil
|
|
290
|
+
start_timed_out(pid)
|
|
291
|
+
debug "Timeout waiting for daemon to be ready, PID #{pid.inspect}"
|
|
292
|
+
abort_start(pid: pid, is_direct_child: false) if pid
|
|
293
|
+
raise StartTimeout, concat_spawn_output_and_logs(spawn_result ? spawn_result.output : nil,
|
|
294
|
+
differences_in_log_file, nil, "timed out")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
case spawn_result
|
|
298
|
+
when InternalCommandOkResult
|
|
299
|
+
success, _ = timeoutable(remaining_time) { allow_timeout { run_ping_command } }
|
|
300
|
+
if success
|
|
301
|
+
true
|
|
417
302
|
else
|
|
418
|
-
|
|
303
|
+
raise StartError, concat_spawn_output_and_logs(spawn_result.output, differences_in_log_file)
|
|
419
304
|
end
|
|
420
|
-
end
|
|
421
305
|
|
|
422
|
-
|
|
423
|
-
raise StartError, concat_spawn_output_and_logs(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
306
|
+
when InternalCommandErrorResult
|
|
307
|
+
raise StartError, concat_spawn_output_and_logs(spawn_result.output,
|
|
308
|
+
differences_in_log_file, spawn_result.exit_status)
|
|
309
|
+
|
|
310
|
+
when InternalCommandTimeoutResult
|
|
311
|
+
daemonization_timed_out(spawn_result.pid)
|
|
312
|
+
abort_start(pid: spawn_result.pid, is_direct_child: true)
|
|
313
|
+
debug "Timeout waiting for daemon to fork, PID #{spawn_result.pid}"
|
|
314
|
+
raise StartTimeout, concat_spawn_output_and_logs(spawn_result.output,
|
|
315
|
+
differences_in_log_file, nil, "timed out")
|
|
316
|
+
|
|
428
317
|
else
|
|
429
|
-
|
|
318
|
+
raise "Bug: unexpected result from #spawn_daemon: #{spawn_result.inspect}"
|
|
430
319
|
end
|
|
431
320
|
end
|
|
432
321
|
|
|
@@ -437,54 +326,48 @@ class DaemonController
|
|
|
437
326
|
end
|
|
438
327
|
|
|
439
328
|
def spawn_daemon
|
|
440
|
-
|
|
329
|
+
if @start_command.respond_to?(:call)
|
|
441
330
|
run_command(@start_command.call)
|
|
442
331
|
else
|
|
443
332
|
run_command(@start_command)
|
|
444
333
|
end
|
|
445
|
-
rescue DaemonizationTimeout => e
|
|
446
|
-
@spawn_output = e.output
|
|
447
|
-
raise e
|
|
448
334
|
end
|
|
449
335
|
|
|
450
336
|
def kill_daemon
|
|
451
337
|
if @stop_command
|
|
452
|
-
if @dont_stop_if_pid_file_invalid && read_pid_file.nil?
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
338
|
+
return if @dont_stop_if_pid_file_invalid && read_pid_file.nil?
|
|
339
|
+
|
|
340
|
+
result = run_command(@stop_command)
|
|
341
|
+
case result
|
|
342
|
+
when InternalCommandOkResult
|
|
343
|
+
# Success
|
|
344
|
+
when InternalCommandErrorResult
|
|
345
|
+
raise StopError, concat_spawn_output_and_logs(result.output, nil, result.exit_status)
|
|
346
|
+
when InternalCommandTimeoutResult
|
|
347
|
+
raise StopError, concat_spawn_output_and_logs(result.output, nil, nil, "timed out")
|
|
348
|
+
else
|
|
349
|
+
raise "Bug: unexpected result from #run_command: #{result.inspect}"
|
|
459
350
|
end
|
|
460
351
|
else
|
|
461
352
|
kill_daemon_with_signal
|
|
462
353
|
end
|
|
463
354
|
end
|
|
464
355
|
|
|
465
|
-
def kill_daemon_with_signal(force
|
|
466
|
-
pid = read_pid_file
|
|
467
|
-
if pid
|
|
356
|
+
def kill_daemon_with_signal(force: false)
|
|
357
|
+
if (pid = read_pid_file)
|
|
468
358
|
if force
|
|
469
359
|
Process.kill("SIGKILL", pid)
|
|
470
360
|
else
|
|
471
|
-
Process.kill(
|
|
361
|
+
Process.kill(normalize_signal_name(@stop_graceful_signal), pid)
|
|
472
362
|
end
|
|
473
363
|
end
|
|
474
364
|
rescue Errno::ESRCH, Errno::ENOENT
|
|
475
365
|
end
|
|
476
366
|
|
|
477
367
|
def daemon_is_running?
|
|
478
|
-
|
|
479
|
-
pid = read_pid_file
|
|
480
|
-
rescue Errno::ENOENT
|
|
481
|
-
# The PID file may not exist, or another thread/process
|
|
482
|
-
# executing #running? may have just deleted the PID file.
|
|
483
|
-
# So we catch this error.
|
|
484
|
-
pid = nil
|
|
485
|
-
end
|
|
368
|
+
pid = read_pid_file
|
|
486
369
|
if pid.nil?
|
|
487
|
-
|
|
370
|
+
nil
|
|
488
371
|
elsif check_pid(pid)
|
|
489
372
|
true
|
|
490
373
|
else
|
|
@@ -494,14 +377,11 @@ class DaemonController
|
|
|
494
377
|
end
|
|
495
378
|
|
|
496
379
|
def read_pid_file
|
|
497
|
-
|
|
498
|
-
pid = File.read(@pid_file).strip
|
|
499
|
-
rescue Errno::ENOENT
|
|
500
|
-
return nil
|
|
501
|
-
end
|
|
380
|
+
pid = File.read(@pid_file).strip
|
|
502
381
|
if /\A\d+\Z/.match?(pid)
|
|
503
382
|
pid.to_i
|
|
504
383
|
end
|
|
384
|
+
rescue Errno::ENOENT
|
|
505
385
|
end
|
|
506
386
|
|
|
507
387
|
def delete_pid_file
|
|
@@ -522,7 +402,7 @@ class DaemonController
|
|
|
522
402
|
true
|
|
523
403
|
end
|
|
524
404
|
|
|
525
|
-
def wait_until(sleep_interval
|
|
405
|
+
def wait_until(sleep_interval: 0.1)
|
|
526
406
|
until yield
|
|
527
407
|
sleep(sleep_interval)
|
|
528
408
|
end
|
|
@@ -552,15 +432,69 @@ class DaemonController
|
|
|
552
432
|
end
|
|
553
433
|
|
|
554
434
|
def pid_file_available?
|
|
555
|
-
File.exist?(@pid_file) && File.
|
|
435
|
+
File.exist?(@pid_file) && !File.zero?(@pid_file)
|
|
556
436
|
end
|
|
557
437
|
|
|
558
438
|
# This method does nothing and only serves as a hook for the unit test.
|
|
559
|
-
def
|
|
439
|
+
def daemon_spawned
|
|
560
440
|
end
|
|
561
441
|
|
|
562
442
|
# This method does nothing and only serves as a hook for the unit test.
|
|
563
|
-
def
|
|
443
|
+
def start_timed_out(pid)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# This method does nothing and only serves as a hook for the unit test.
|
|
447
|
+
def daemonization_timed_out(pid)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Aborts a daemon that we tried to start, but timed out.
|
|
451
|
+
def abort_start(pid:, is_direct_child:)
|
|
452
|
+
begin
|
|
453
|
+
debug "Killing process #{pid}"
|
|
454
|
+
Process.kill("SIGTERM", pid)
|
|
455
|
+
rescue SystemCallError
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
begin
|
|
459
|
+
timeoutable(@start_abort_timeout) do
|
|
460
|
+
allow_timeout do
|
|
461
|
+
wait_for_aborted_process(pid:, is_direct_child:)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
rescue Timeout::Error
|
|
465
|
+
begin
|
|
466
|
+
Process.kill("SIGKILL", pid)
|
|
467
|
+
rescue SystemCallError
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
allow_timeout do
|
|
471
|
+
wait_for_aborted_process(pid:, is_direct_child:)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def wait_for_aborted_process(pid:, is_direct_child:)
|
|
477
|
+
if is_direct_child
|
|
478
|
+
begin
|
|
479
|
+
debug "Waiting directly for process #{pid}"
|
|
480
|
+
Process.waitpid(pid)
|
|
481
|
+
rescue SystemCallError
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# The daemon may have:
|
|
485
|
+
# 1. Written a PID file before forking. We delete this PID file.
|
|
486
|
+
# -OR-
|
|
487
|
+
# 2. It might have forked (and written a PID file) right before
|
|
488
|
+
# we terminated it. We'll want the fork to stay alive rather
|
|
489
|
+
# than going through the (complicated) trouble of killing it.
|
|
490
|
+
# Don't touch the PID file.
|
|
491
|
+
pid2 = read_pid_file
|
|
492
|
+
debug "PID file contains #{pid2.inspect}"
|
|
493
|
+
delete_pid_file if pid == pid2
|
|
494
|
+
else
|
|
495
|
+
debug "Waiting until daemon is no longer running"
|
|
496
|
+
wait_until { !daemon_is_running? }
|
|
497
|
+
end
|
|
564
498
|
end
|
|
565
499
|
|
|
566
500
|
def save_log_file_information
|
|
@@ -604,9 +538,9 @@ class DaemonController
|
|
|
604
538
|
nil
|
|
605
539
|
end
|
|
606
540
|
|
|
607
|
-
def determine_lock_file(
|
|
608
|
-
if
|
|
609
|
-
LockFile.new(File.absolute_path(
|
|
541
|
+
def determine_lock_file(given_lock_file, identifier, pid_file)
|
|
542
|
+
if given_lock_file
|
|
543
|
+
LockFile.new(File.absolute_path(given_lock_file))
|
|
610
544
|
else
|
|
611
545
|
LockFile.new(File.absolute_path(pid_file + ".lock"))
|
|
612
546
|
end
|
|
@@ -614,64 +548,44 @@ class DaemonController
|
|
|
614
548
|
|
|
615
549
|
def run_command(command)
|
|
616
550
|
if should_capture_output_while_running_command?
|
|
617
|
-
|
|
551
|
+
# Create tempfile for storing the command's output.
|
|
552
|
+
tempfile = Tempfile.new("daemon-output")
|
|
553
|
+
tempfile.chmod(0o666)
|
|
554
|
+
tempfile_path = tempfile.path
|
|
555
|
+
tempfile.close
|
|
556
|
+
|
|
557
|
+
spawn_options = {
|
|
558
|
+
in: "/dev/null",
|
|
559
|
+
out: tempfile_path,
|
|
560
|
+
err: tempfile_path,
|
|
561
|
+
close_others: true
|
|
562
|
+
}
|
|
618
563
|
else
|
|
619
|
-
|
|
564
|
+
spawn_options = {
|
|
565
|
+
in: "/dev/null",
|
|
566
|
+
out: :out,
|
|
567
|
+
err: :err,
|
|
568
|
+
close_others: true
|
|
569
|
+
}
|
|
620
570
|
end
|
|
621
|
-
end
|
|
622
571
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
else
|
|
627
|
-
begin
|
|
628
|
-
real_log_file = Pathname.new(@log_file).realpath.to_s
|
|
629
|
-
rescue SystemCallError
|
|
630
|
-
real_log_file = nil
|
|
631
|
-
end
|
|
632
|
-
if real_log_file
|
|
633
|
-
!is_std_channel_chardev?(real_log_file)
|
|
634
|
-
else
|
|
635
|
-
true
|
|
572
|
+
if @keep_ios
|
|
573
|
+
@keep_ios.each do |io|
|
|
574
|
+
spawn_options[io] = io
|
|
636
575
|
end
|
|
637
576
|
end
|
|
638
|
-
end
|
|
639
577
|
|
|
640
|
-
def is_std_channel_chardev?(path)
|
|
641
|
-
path == "/dev/stdout" ||
|
|
642
|
-
path == "/dev/stderr" ||
|
|
643
|
-
path == "/dev/fd/1" ||
|
|
644
|
-
path == "/dev/fd/2" ||
|
|
645
|
-
path =~ %r{\A/proc/([0-9]+|self)/fd/[12]\Z}
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
def run_command_while_capturing_output(command)
|
|
649
|
-
# Create tempfile for storing the command's output.
|
|
650
|
-
tempfile = Tempfile.new("daemon-output")
|
|
651
|
-
tempfile.chmod(0o666)
|
|
652
|
-
tempfile_path = tempfile.path
|
|
653
|
-
tempfile.close
|
|
654
|
-
|
|
655
|
-
options = {
|
|
656
|
-
in: "/dev/null",
|
|
657
|
-
out: tempfile_path,
|
|
658
|
-
err: tempfile_path,
|
|
659
|
-
close_others: true
|
|
660
|
-
}
|
|
661
|
-
@keep_ios.each do |io|
|
|
662
|
-
options[io] = io
|
|
663
|
-
end
|
|
664
578
|
pid = if @daemonize_for_me
|
|
665
579
|
Process.spawn(@env, ruby_interpreter, SPAWNER_FILE,
|
|
666
|
-
command,
|
|
580
|
+
command, spawn_options)
|
|
667
581
|
else
|
|
668
|
-
Process.spawn(@env, command,
|
|
582
|
+
Process.spawn(@env, command, spawn_options)
|
|
669
583
|
end
|
|
670
584
|
|
|
671
585
|
# run_command might be running in a timeout block (like
|
|
672
586
|
# in #start_without_locking).
|
|
673
587
|
begin
|
|
674
|
-
|
|
588
|
+
Process.waitpid(pid)
|
|
675
589
|
rescue Errno::ECHILD
|
|
676
590
|
# Maybe a background thread or whatever waitpid()'ed
|
|
677
591
|
# this child process before we had the chance. There's
|
|
@@ -679,102 +593,51 @@ class DaemonController
|
|
|
679
593
|
# it started successfully; if it didn't we'll know
|
|
680
594
|
# that later by checking the PID file and by pinging
|
|
681
595
|
# it.
|
|
682
|
-
return
|
|
596
|
+
return InternalCommandOkResult.new(pid, tempfile_path && File.read(tempfile_path).strip)
|
|
683
597
|
rescue Timeout::Error
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
# If the daemon doesn't fork into the background
|
|
687
|
-
# in time, then kill it.
|
|
688
|
-
begin
|
|
689
|
-
Process.kill("SIGTERM", pid)
|
|
690
|
-
rescue SystemCallError
|
|
691
|
-
end
|
|
692
|
-
begin
|
|
693
|
-
Timeout.timeout(5, Timeout::Error) do
|
|
694
|
-
interruptable_waitpid(pid)
|
|
695
|
-
rescue SystemCallError
|
|
696
|
-
end
|
|
697
|
-
rescue Timeout::Error
|
|
698
|
-
begin
|
|
699
|
-
Process.kill("SIGKILL", pid)
|
|
700
|
-
interruptable_waitpid(pid)
|
|
701
|
-
rescue SystemCallError
|
|
702
|
-
end
|
|
703
|
-
end
|
|
704
|
-
raise DaemonizationTimeout, File.read(tempfile_path).strip
|
|
598
|
+
return InternalCommandTimeoutResult.new(pid, tempfile_path && File.read(tempfile_path).strip)
|
|
705
599
|
end
|
|
706
600
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
601
|
+
child_status = $?
|
|
602
|
+
output = File.read(tempfile_path).strip if tempfile_path
|
|
603
|
+
if child_status.success?
|
|
604
|
+
InternalCommandOkResult.new(pid, output)
|
|
710
605
|
else
|
|
711
|
-
output
|
|
606
|
+
InternalCommandErrorResult.new(pid, output, child_status)
|
|
712
607
|
end
|
|
713
608
|
ensure
|
|
714
609
|
begin
|
|
715
|
-
|
|
716
|
-
rescue
|
|
610
|
+
tempfile.unlink if tempfile
|
|
611
|
+
rescue SystemCallError
|
|
717
612
|
nil
|
|
718
613
|
end
|
|
719
614
|
end
|
|
720
615
|
|
|
721
|
-
def
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
out: :out,
|
|
725
|
-
err: :err,
|
|
726
|
-
close_others: true
|
|
727
|
-
}
|
|
728
|
-
@keep_ios.each do |io|
|
|
729
|
-
options[io] = io
|
|
730
|
-
end
|
|
731
|
-
pid = if @daemonize_for_me
|
|
732
|
-
Process.spawn(@env, ruby_interpreter, SPAWNER_FILE,
|
|
733
|
-
command, options)
|
|
616
|
+
def should_capture_output_while_running_command?
|
|
617
|
+
if is_std_channel_chardev?(@log_file)
|
|
618
|
+
false
|
|
734
619
|
else
|
|
735
|
-
Process.spawn(@env, command, options)
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
# run_command might be running in a timeout block (like
|
|
739
|
-
# in #start_without_locking).
|
|
740
|
-
begin
|
|
741
|
-
interruptable_waitpid(pid)
|
|
742
|
-
rescue Errno::ECHILD
|
|
743
|
-
# Maybe a background thread or whatever waitpid()'ed
|
|
744
|
-
# this child process before we had the chance. There's
|
|
745
|
-
# no way to obtain the exit status now. Assume that
|
|
746
|
-
# it started successfully; if it didn't we'll know
|
|
747
|
-
# that later by checking the PID file and by pinging
|
|
748
|
-
# it.
|
|
749
|
-
return
|
|
750
|
-
rescue Timeout::Error
|
|
751
|
-
daemonization_timed_out
|
|
752
|
-
|
|
753
|
-
# If the daemon doesn't fork into the background
|
|
754
|
-
# in time, then kill it.
|
|
755
620
|
begin
|
|
756
|
-
|
|
621
|
+
real_log_file = Pathname.new(@log_file).realpath.to_s
|
|
757
622
|
rescue SystemCallError
|
|
623
|
+
real_log_file = nil
|
|
758
624
|
end
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
end
|
|
764
|
-
rescue Timeout::Error
|
|
765
|
-
begin
|
|
766
|
-
Process.kill("SIGKILL", pid)
|
|
767
|
-
interruptable_waitpid(pid)
|
|
768
|
-
rescue SystemCallError
|
|
769
|
-
end
|
|
625
|
+
if real_log_file
|
|
626
|
+
!is_std_channel_chardev?(real_log_file)
|
|
627
|
+
else
|
|
628
|
+
true
|
|
770
629
|
end
|
|
771
|
-
raise DaemonizationTimeout, nil
|
|
772
|
-
end
|
|
773
|
-
if $?.exitstatus != 0
|
|
774
|
-
raise StartError, concat_spawn_output_and_logs(nil, differences_in_log_file, $?)
|
|
775
630
|
end
|
|
776
631
|
end
|
|
777
632
|
|
|
633
|
+
def is_std_channel_chardev?(path)
|
|
634
|
+
path == "/dev/stdout" ||
|
|
635
|
+
path == "/dev/stderr" ||
|
|
636
|
+
path == "/dev/fd/1" ||
|
|
637
|
+
path == "/dev/fd/2" ||
|
|
638
|
+
path =~ %r{\A/proc/([0-9]+|self)/fd/[12]\Z}
|
|
639
|
+
end
|
|
640
|
+
|
|
778
641
|
def run_ping_command
|
|
779
642
|
if @ping_command.respond_to?(:call)
|
|
780
643
|
begin
|
|
@@ -821,43 +684,7 @@ class DaemonController
|
|
|
821
684
|
end
|
|
822
685
|
end
|
|
823
686
|
|
|
824
|
-
if
|
|
825
|
-
require "java"
|
|
826
|
-
|
|
827
|
-
def ping_socket(host_name, port)
|
|
828
|
-
channel = java.nio.channels.SocketChannel.open
|
|
829
|
-
begin
|
|
830
|
-
address = java.net.InetSocketAddress.new(host_name, port)
|
|
831
|
-
channel.configure_blocking(false)
|
|
832
|
-
if channel.connect(address)
|
|
833
|
-
return true
|
|
834
|
-
end
|
|
835
|
-
|
|
836
|
-
deadline = Time.now.to_f + 0.1
|
|
837
|
-
while true
|
|
838
|
-
begin
|
|
839
|
-
if channel.finish_connect
|
|
840
|
-
return true
|
|
841
|
-
end
|
|
842
|
-
rescue java.net.ConnectException => e
|
|
843
|
-
if /Connection refused/i.match?(e.message)
|
|
844
|
-
return false
|
|
845
|
-
else
|
|
846
|
-
throw e
|
|
847
|
-
end
|
|
848
|
-
end
|
|
849
|
-
|
|
850
|
-
# Not done connecting and no error.
|
|
851
|
-
sleep 0.01
|
|
852
|
-
if Time.now.to_f >= deadline
|
|
853
|
-
return false
|
|
854
|
-
end
|
|
855
|
-
end
|
|
856
|
-
ensure
|
|
857
|
-
channel.close
|
|
858
|
-
end
|
|
859
|
-
end
|
|
860
|
-
else
|
|
687
|
+
if can_ping_unix_sockets?
|
|
861
688
|
def ping_socket(socket_domain, sockaddr)
|
|
862
689
|
socket = Socket.new(socket_domain, Socket::Constants::SOCK_STREAM, 0)
|
|
863
690
|
begin
|
|
@@ -890,31 +717,59 @@ class DaemonController
|
|
|
890
717
|
rescue Errno::EAFNOSUPPORT
|
|
891
718
|
ping_socket(Socket::Constants::AF_INET6, sockaddr)
|
|
892
719
|
end
|
|
720
|
+
else
|
|
721
|
+
require "java"
|
|
722
|
+
|
|
723
|
+
def ping_socket(host_name, port)
|
|
724
|
+
channel = java.nio.channels.SocketChannel.open
|
|
725
|
+
begin
|
|
726
|
+
address = java.net.InetSocketAddress.new(host_name, port)
|
|
727
|
+
channel.configure_blocking(false)
|
|
728
|
+
return true if channel.connect(address)
|
|
729
|
+
|
|
730
|
+
deadline = Time.now.to_f + 0.1
|
|
731
|
+
loop do
|
|
732
|
+
begin
|
|
733
|
+
return true if channel.finish_connect
|
|
734
|
+
rescue java.net.ConnectException => e
|
|
735
|
+
if /Connection refused/i.match?(e.message)
|
|
736
|
+
return false
|
|
737
|
+
else
|
|
738
|
+
throw e
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Not done connecting and no error.
|
|
743
|
+
sleep 0.01
|
|
744
|
+
return false if Time.now.to_f >= deadline
|
|
745
|
+
end
|
|
746
|
+
ensure
|
|
747
|
+
channel.close
|
|
748
|
+
end
|
|
749
|
+
end
|
|
893
750
|
end
|
|
894
751
|
|
|
895
752
|
def ruby_interpreter
|
|
896
|
-
rb_config = if defined?(RbConfig)
|
|
897
|
-
RbConfig::CONFIG
|
|
898
|
-
else
|
|
899
|
-
Config::CONFIG
|
|
900
|
-
end
|
|
901
753
|
File.join(
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
)
|
|
754
|
+
RbConfig::CONFIG["bindir"],
|
|
755
|
+
RbConfig::CONFIG.values_at("RUBY_INSTALL_NAME", "EXEEXT").join
|
|
756
|
+
)
|
|
905
757
|
end
|
|
906
758
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
result = nil
|
|
913
|
-
until result
|
|
914
|
-
result = Process.waitpid(pid, Process::WNOHANG)
|
|
915
|
-
sleep 0.01 if !result
|
|
759
|
+
def timeoutable(amount, &block)
|
|
760
|
+
Thread.handle_interrupt(Timeout::Error => :never) do
|
|
761
|
+
start_time = monotonic_time
|
|
762
|
+
result = Timeout.timeout(amount, Timeout::Error, &block)
|
|
763
|
+
[result, [monotonic_time - start_time, 0].max]
|
|
916
764
|
end
|
|
917
|
-
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def allow_timeout(&block)
|
|
768
|
+
Thread.handle_interrupt(Timeout::Error => :on_blocking, &block)
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def monotonic_time
|
|
772
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
918
773
|
end
|
|
919
774
|
|
|
920
775
|
def signal_termination_message(process_status)
|
|
@@ -925,8 +780,12 @@ class DaemonController
|
|
|
925
780
|
end
|
|
926
781
|
end
|
|
927
782
|
|
|
783
|
+
def normalize_signal_name(name)
|
|
784
|
+
name.start_with?("SIG") ? name : "SIG#{name}"
|
|
785
|
+
end
|
|
786
|
+
|
|
928
787
|
def signal_name_for(num)
|
|
929
|
-
if (name = Signal.list.
|
|
788
|
+
if (name = Signal.list.key(num))
|
|
930
789
|
"SIG#{name}"
|
|
931
790
|
else
|
|
932
791
|
num.to_s
|
|
@@ -934,30 +793,31 @@ class DaemonController
|
|
|
934
793
|
end
|
|
935
794
|
|
|
936
795
|
def concat_spawn_output_and_logs(output, logs, exit_status = nil, suffix_message = nil)
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
796
|
+
format_full_suffix_message = lambda do |main_message = nil|
|
|
797
|
+
[
|
|
798
|
+
main_message,
|
|
940
799
|
exit_status ? signal_termination_message(exit_status) : nil,
|
|
941
800
|
suffix_message
|
|
942
801
|
].compact.join("; ")
|
|
943
|
-
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
if output.nil? && logs.nil?
|
|
805
|
+
"(#{format_full_suffix_message.call("logs not available")})"
|
|
944
806
|
elsif (output && output.empty? && logs && logs.empty?) || (output && output.empty? && logs.nil?) || (output.nil? && logs && logs.empty?)
|
|
945
|
-
|
|
946
|
-
"logs empty",
|
|
947
|
-
exit_status ? signal_termination_message(exit_status) : nil,
|
|
948
|
-
suffix_message
|
|
949
|
-
].compact.join("; ")
|
|
950
|
-
"(#{result_inner})"
|
|
807
|
+
"(#{format_full_suffix_message.call("logs empty")})"
|
|
951
808
|
else
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
809
|
+
full_suffix_message = format_full_suffix_message.call
|
|
810
|
+
if full_suffix_message.empty?
|
|
811
|
+
"#{output}\n#{logs}".strip
|
|
812
|
+
elsif logs && logs.empty?
|
|
813
|
+
"#{output}\n(#{full_suffix_message})".strip
|
|
814
|
+
else
|
|
815
|
+
"#{output}\n#{logs}\n(#{full_suffix_message})".strip
|
|
959
816
|
end
|
|
960
|
-
result
|
|
961
817
|
end
|
|
962
818
|
end
|
|
819
|
+
|
|
820
|
+
def debug(message)
|
|
821
|
+
@logger.debug(message) if @logger
|
|
822
|
+
end
|
|
963
823
|
end
|