serverengine 2.0.0pre1-x86-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +20 -0
- data/Changelog +122 -0
- data/Gemfile +2 -0
- data/LICENSE +202 -0
- data/NOTICE +3 -0
- data/README.md +514 -0
- data/Rakefile +26 -0
- data/appveyor.yml +24 -0
- data/examples/server.rb +138 -0
- data/examples/spawn_worker_script.rb +38 -0
- data/lib/serverengine.rb +46 -0
- data/lib/serverengine/blocking_flag.rb +77 -0
- data/lib/serverengine/command_sender.rb +89 -0
- data/lib/serverengine/config_loader.rb +82 -0
- data/lib/serverengine/daemon.rb +233 -0
- data/lib/serverengine/daemon_logger.rb +135 -0
- data/lib/serverengine/embedded_server.rb +67 -0
- data/lib/serverengine/multi_process_server.rb +155 -0
- data/lib/serverengine/multi_spawn_server.rb +95 -0
- data/lib/serverengine/multi_thread_server.rb +80 -0
- data/lib/serverengine/multi_worker_server.rb +150 -0
- data/lib/serverengine/privilege.rb +57 -0
- data/lib/serverengine/process_manager.rb +508 -0
- data/lib/serverengine/server.rb +178 -0
- data/lib/serverengine/signal_thread.rb +116 -0
- data/lib/serverengine/signals.rb +31 -0
- data/lib/serverengine/socket_manager.rb +171 -0
- data/lib/serverengine/socket_manager_unix.rb +98 -0
- data/lib/serverengine/socket_manager_win.rb +154 -0
- data/lib/serverengine/supervisor.rb +313 -0
- data/lib/serverengine/utils.rb +62 -0
- data/lib/serverengine/version.rb +3 -0
- data/lib/serverengine/winsock.rb +128 -0
- data/lib/serverengine/worker.rb +81 -0
- data/serverengine.gemspec +37 -0
- data/spec/blocking_flag_spec.rb +59 -0
- data/spec/daemon_logger_spec.rb +175 -0
- data/spec/daemon_spec.rb +169 -0
- data/spec/multi_process_server_spec.rb +113 -0
- data/spec/server_worker_context.rb +232 -0
- data/spec/signal_thread_spec.rb +94 -0
- data/spec/socket_manager_spec.rb +119 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/supervisor_spec.rb +215 -0
- metadata +184 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
#
|
2
|
+
# ServerEngine
|
3
|
+
#
|
4
|
+
# Copyright (C) 2012-2013 FURUHASHI Sadayuki
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require 'etc'
|
19
|
+
|
20
|
+
module ServerEngine
|
21
|
+
module Privilege
|
22
|
+
def self.get_etc_passwd(user)
|
23
|
+
if user.to_i.to_s == user
|
24
|
+
Etc.getpwuid(user.to_i)
|
25
|
+
else
|
26
|
+
Etc.getpwnam(user)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.get_etc_group(group)
|
31
|
+
if group.to_i.to_s == group
|
32
|
+
Etc.getgrgid(group.to_i)
|
33
|
+
else
|
34
|
+
Etc.getgrnam(group)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.change(user, group)
|
39
|
+
if user
|
40
|
+
etc_pw = get_etc_passwd(user)
|
41
|
+
user_groups = [etc_pw.gid]
|
42
|
+
Etc.setgrent
|
43
|
+
Etc.group { |gr| user_groups << gr.gid if gr.mem.include?(etc_pw.name) } # emulate 'id -G'
|
44
|
+
|
45
|
+
Process.groups = Process.groups | user_groups
|
46
|
+
Process::UID.change_privilege(etc_pw.uid)
|
47
|
+
end
|
48
|
+
|
49
|
+
if group
|
50
|
+
etc_group = get_etc_group(group)
|
51
|
+
Process::GID.change_privilege(etc_group.gid)
|
52
|
+
end
|
53
|
+
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,508 @@
|
|
1
|
+
#
|
2
|
+
# ServerEngine
|
3
|
+
#
|
4
|
+
# Copyright (C) 2012-2013 Sadayuki Furuhashi
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require 'fcntl'
|
19
|
+
|
20
|
+
module ServerEngine
|
21
|
+
|
22
|
+
class ProcessManager
|
23
|
+
def initialize(config={})
|
24
|
+
@monitors = []
|
25
|
+
@rpipes = {}
|
26
|
+
@heartbeat_time = {}
|
27
|
+
|
28
|
+
@cloexec_mode = config[:cloexec_mode]
|
29
|
+
|
30
|
+
@graceful_kill_signal = config[:graceful_kill_signal] || :TERM
|
31
|
+
@immediate_kill_signal = config[:immediate_kill_signal] || :QUIT
|
32
|
+
|
33
|
+
@auto_tick = !!config.fetch(:auto_tick, true)
|
34
|
+
@auto_tick_interval = config[:auto_tick_interval] || 1
|
35
|
+
|
36
|
+
@enable_heartbeat = !!config[:enable_heartbeat]
|
37
|
+
if ServerEngine.windows?
|
38
|
+
# heartbeat is not supported on Windows platform. See also spawn method.
|
39
|
+
@enable_heartbeat = false
|
40
|
+
end
|
41
|
+
@auto_heartbeat = !!config.fetch(:auto_heartbeat, true)
|
42
|
+
|
43
|
+
case op = config[:on_heartbeat_error]
|
44
|
+
when nil
|
45
|
+
@heartbeat_error_proc = lambda {|t| }
|
46
|
+
when Proc
|
47
|
+
@heartbeat_error_proc = op
|
48
|
+
when :abort
|
49
|
+
@heartbeat_error_proc = lambda {|t| exit 1 }
|
50
|
+
else
|
51
|
+
raise ArgumentError, "unexpected :on_heartbeat_error option (expected Proc, true or false but got #{op.class})"
|
52
|
+
end
|
53
|
+
|
54
|
+
configure(config)
|
55
|
+
|
56
|
+
@closed = false
|
57
|
+
@read_buffer = ''
|
58
|
+
|
59
|
+
if @auto_tick
|
60
|
+
TickThread.new(@auto_tick_interval, &method(:tick))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_accessor :logger
|
65
|
+
|
66
|
+
attr_accessor :cloexec_mode
|
67
|
+
|
68
|
+
attr_reader :graceful_kill_signal, :immediate_kill_signal
|
69
|
+
attr_reader :auto_tick, :auto_tick_interval
|
70
|
+
attr_reader :enable_heartbeat, :auto_heartbeat
|
71
|
+
|
72
|
+
attr_accessor :command_sender
|
73
|
+
attr_reader :command_sender_pipe
|
74
|
+
|
75
|
+
CONFIG_PARAMS = {
|
76
|
+
heartbeat_interval: 1,
|
77
|
+
heartbeat_timeout: 180,
|
78
|
+
graceful_kill_interval: 15,
|
79
|
+
graceful_kill_interval_increment: 10,
|
80
|
+
graceful_kill_timeout: 600,
|
81
|
+
immediate_kill_interval: 10,
|
82
|
+
immediate_kill_interval_increment: 10,
|
83
|
+
immediate_kill_timeout: 600,
|
84
|
+
}
|
85
|
+
|
86
|
+
CONFIG_PARAMS.each_pair do |key,default_value|
|
87
|
+
attr_reader key
|
88
|
+
|
89
|
+
define_method("#{key}=") do |v|
|
90
|
+
v = default_value if v == nil
|
91
|
+
instance_variable_set("@#{key}", v)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def configure(config, opts={})
|
96
|
+
prefix = opts[:prefix] || ""
|
97
|
+
CONFIG_PARAMS.keys.each {|key|
|
98
|
+
send("#{key}=", config[:"#{prefix}#{key}"])
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def monitor_options
|
103
|
+
{
|
104
|
+
enable_heartbeat: @enable_heartbeat,
|
105
|
+
heartbeat_timeout: @heartbeat_timeout,
|
106
|
+
graceful_kill_signal: @graceful_kill_signal,
|
107
|
+
graceful_kill_timeout: @graceful_kill_timeout,
|
108
|
+
graceful_kill_interval: @graceful_kill_interval,
|
109
|
+
graceful_kill_interval_increment: @graceful_kill_interval_increment,
|
110
|
+
immediate_kill_signal: @immediate_kill_signal,
|
111
|
+
immediate_kill_timeout: @immediate_kill_timeout,
|
112
|
+
immediate_kill_interval: @immediate_kill_interval,
|
113
|
+
immediate_kill_interval_increment: @immediate_kill_interval_increment,
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def fork(&block)
|
118
|
+
if ServerEngine.windows?
|
119
|
+
raise NotImplementedError, "fork is not available on this platform. Please use spawn (worker_type: 'spawn')."
|
120
|
+
end
|
121
|
+
|
122
|
+
rpipe, wpipe = new_pipe_pair
|
123
|
+
|
124
|
+
begin
|
125
|
+
pid = Process.fork do
|
126
|
+
self.close
|
127
|
+
begin
|
128
|
+
t = Target.new(wpipe)
|
129
|
+
if @enable_heartbeat && @auto_heartbeat
|
130
|
+
HeartbeatThread.new(@heartbeat_interval, t, @heartbeat_error_proc)
|
131
|
+
end
|
132
|
+
|
133
|
+
block.call(t)
|
134
|
+
exit! 0
|
135
|
+
|
136
|
+
rescue
|
137
|
+
ServerEngine.dump_uncaught_error($!)
|
138
|
+
ensure
|
139
|
+
exit! 1
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
m = Monitor.new(pid, monitor_options)
|
144
|
+
|
145
|
+
@monitors << m
|
146
|
+
@rpipes[rpipe] = m
|
147
|
+
rpipe = nil
|
148
|
+
|
149
|
+
return m
|
150
|
+
|
151
|
+
ensure
|
152
|
+
wpipe.close
|
153
|
+
rpipe.close if rpipe
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def spawn(*args)
|
158
|
+
if args.first.is_a?(Hash)
|
159
|
+
env = args.shift.dup
|
160
|
+
else
|
161
|
+
env = {}
|
162
|
+
end
|
163
|
+
|
164
|
+
if args.last.is_a?(Hash)
|
165
|
+
options = args.pop.dup
|
166
|
+
else
|
167
|
+
options = {}
|
168
|
+
end
|
169
|
+
|
170
|
+
# pipe is necessary even if @enable_heartbeat == false because
|
171
|
+
# parent process detects shutdown of a child process using it
|
172
|
+
begin
|
173
|
+
unless ServerEngine.windows?
|
174
|
+
# heartbeat is not supported on Windows platform
|
175
|
+
rpipe, wpipe = new_pipe_pair
|
176
|
+
options[[wpipe.fileno]] = wpipe
|
177
|
+
if @enable_heartbeat
|
178
|
+
env['SERVERENGINE_HEARTBEAT_PIPE'] = wpipe.fileno.to_s
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
if @command_sender == "pipe"
|
183
|
+
inpipe, @command_sender_pipe = IO.pipe
|
184
|
+
@command_sender_pipe.sync = true
|
185
|
+
@command_sender_pipe.binmode
|
186
|
+
options[:in] = inpipe
|
187
|
+
end
|
188
|
+
pid = Process.spawn(env, *args, options)
|
189
|
+
if @command_sender == "pipe"
|
190
|
+
inpipe.close
|
191
|
+
end
|
192
|
+
|
193
|
+
m = Monitor.new(pid, monitor_options)
|
194
|
+
|
195
|
+
@monitors << m
|
196
|
+
|
197
|
+
unless ServerEngine.windows?
|
198
|
+
@rpipes[rpipe] = m
|
199
|
+
rpipe = nil
|
200
|
+
end
|
201
|
+
|
202
|
+
return m
|
203
|
+
|
204
|
+
ensure
|
205
|
+
wpipe.close if wpipe
|
206
|
+
rpipe.close if rpipe
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def new_pipe_pair
|
211
|
+
rpipe, wpipe = IO.pipe
|
212
|
+
|
213
|
+
if Fcntl.const_defined?(:F_SETFD) && Fcntl.const_defined?(:FD_CLOEXEC)
|
214
|
+
case @cloexec_mode
|
215
|
+
when :target_only
|
216
|
+
wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
217
|
+
when :monitor_only
|
218
|
+
rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
219
|
+
else
|
220
|
+
rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
221
|
+
wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
rpipe.sync = true
|
226
|
+
wpipe.sync = true
|
227
|
+
|
228
|
+
return rpipe, wpipe
|
229
|
+
end
|
230
|
+
|
231
|
+
def close
|
232
|
+
@closed = true
|
233
|
+
@rpipes.keys.each {|m| m.close }
|
234
|
+
nil
|
235
|
+
end
|
236
|
+
|
237
|
+
def tick(blocking_timeout=0)
|
238
|
+
if @closed
|
239
|
+
raise AlreadyClosedError.new
|
240
|
+
end
|
241
|
+
|
242
|
+
time ||= Time.now
|
243
|
+
|
244
|
+
unless ServerEngine.windows?
|
245
|
+
# heartbeat is not supported on Windows platform.
|
246
|
+
|
247
|
+
if @rpipes.empty?
|
248
|
+
sleep blocking_timeout if blocking_timeout > 0
|
249
|
+
return nil
|
250
|
+
end
|
251
|
+
|
252
|
+
ready_pipes, _, _ = IO.select(@rpipes.keys, nil, nil, blocking_timeout)
|
253
|
+
|
254
|
+
if ready_pipes
|
255
|
+
ready_pipes.each do |r|
|
256
|
+
begin
|
257
|
+
r.read_nonblock(1024, @read_buffer)
|
258
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
259
|
+
next
|
260
|
+
rescue #EOFError
|
261
|
+
m = @rpipes.delete(r)
|
262
|
+
m.start_immediate_stop!
|
263
|
+
r.close rescue nil
|
264
|
+
next
|
265
|
+
end
|
266
|
+
|
267
|
+
if m = @rpipes[r]
|
268
|
+
m.last_heartbeat_time = time
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
@monitors.delete_if {|m|
|
275
|
+
!m.tick(time)
|
276
|
+
}
|
277
|
+
|
278
|
+
nil
|
279
|
+
end
|
280
|
+
|
281
|
+
class AlreadyClosedError < EOFError
|
282
|
+
end
|
283
|
+
|
284
|
+
HEARTBEAT_MESSAGE = [0].pack('C')
|
285
|
+
|
286
|
+
class Monitor
|
287
|
+
def initialize(pid, opts={})
|
288
|
+
@pid = pid
|
289
|
+
|
290
|
+
@enable_heartbeat = opts[:enable_heartbeat]
|
291
|
+
@heartbeat_timeout = opts[:heartbeat_timeout]
|
292
|
+
|
293
|
+
@graceful_kill_signal = opts[:graceful_kill_signal]
|
294
|
+
@graceful_kill_timeout = opts[:graceful_kill_timeout]
|
295
|
+
@graceful_kill_interval = opts[:graceful_kill_interval]
|
296
|
+
@graceful_kill_interval_increment = opts[:graceful_kill_interval_increment]
|
297
|
+
|
298
|
+
@immediate_kill_signal = opts[:immediate_kill_signal]
|
299
|
+
@immediate_kill_timeout = opts[:immediate_kill_timeout]
|
300
|
+
@immediate_kill_interval = opts[:immediate_kill_interval]
|
301
|
+
@immediate_kill_interval_increment = opts[:immediate_kill_interval_increment]
|
302
|
+
|
303
|
+
@error = false
|
304
|
+
@last_heartbeat_time = Time.now
|
305
|
+
@next_kill_time = nil
|
306
|
+
@graceful_kill_start_time = nil
|
307
|
+
@immediate_kill_start_time = nil
|
308
|
+
@kill_count = 0
|
309
|
+
end
|
310
|
+
|
311
|
+
attr_accessor :last_heartbeat_time
|
312
|
+
attr_reader :pid
|
313
|
+
|
314
|
+
def heartbeat_delay
|
315
|
+
now = Time.now
|
316
|
+
now - @last_heartbeat_time
|
317
|
+
end
|
318
|
+
|
319
|
+
def send_signal(sig)
|
320
|
+
pid = @pid
|
321
|
+
return nil unless pid
|
322
|
+
|
323
|
+
begin
|
324
|
+
Process.kill(sig, pid)
|
325
|
+
return true
|
326
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
327
|
+
return false
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def try_join
|
332
|
+
pid = @pid
|
333
|
+
return true unless pid
|
334
|
+
|
335
|
+
begin
|
336
|
+
pid, status = Process.waitpid2(pid, Process::WNOHANG)
|
337
|
+
code = status
|
338
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
339
|
+
# assume that any errors mean the child process is dead
|
340
|
+
code = $!
|
341
|
+
end
|
342
|
+
|
343
|
+
if code
|
344
|
+
@pid = nil
|
345
|
+
return code
|
346
|
+
end
|
347
|
+
|
348
|
+
return false
|
349
|
+
end
|
350
|
+
|
351
|
+
def join
|
352
|
+
pid = @pid
|
353
|
+
return nil unless pid
|
354
|
+
|
355
|
+
begin
|
356
|
+
pid, status = Process.waitpid2(pid)
|
357
|
+
code = status
|
358
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
359
|
+
# assume that any errors mean the child process is dead
|
360
|
+
code = $!
|
361
|
+
end
|
362
|
+
@pid = nil
|
363
|
+
|
364
|
+
return code
|
365
|
+
end
|
366
|
+
|
367
|
+
def start_graceful_stop!
|
368
|
+
now = Time.now
|
369
|
+
@next_kill_time ||= now
|
370
|
+
@graceful_kill_start_time ||= now
|
371
|
+
end
|
372
|
+
|
373
|
+
def start_immediate_stop!
|
374
|
+
now = Time.now
|
375
|
+
@next_kill_time ||= now
|
376
|
+
@immediate_kill_start_time ||= now
|
377
|
+
end
|
378
|
+
|
379
|
+
def tick(now=Time.now)
|
380
|
+
pid = @pid
|
381
|
+
return false unless pid
|
382
|
+
|
383
|
+
if !@immediate_kill_start_time
|
384
|
+
# check heartbeat timeout or escalation
|
385
|
+
if (
|
386
|
+
# heartbeat timeout
|
387
|
+
@enable_heartbeat &&
|
388
|
+
heartbeat_delay >= @heartbeat_timeout
|
389
|
+
) || (
|
390
|
+
# escalation
|
391
|
+
@graceful_kill_start_time &&
|
392
|
+
@graceful_kill_timeout >= 0 &&
|
393
|
+
@graceful_kill_start_time < now - @graceful_kill_timeout
|
394
|
+
)
|
395
|
+
# escalate to immediate kill
|
396
|
+
@kill_count = 0
|
397
|
+
@immediate_kill_start_time = now
|
398
|
+
@next_kill_time = now
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
if !@next_kill_time || @next_kill_time > now
|
403
|
+
# expect next tick
|
404
|
+
return true
|
405
|
+
end
|
406
|
+
|
407
|
+
# send signal now
|
408
|
+
|
409
|
+
if @immediate_kill_start_time
|
410
|
+
interval = @immediate_kill_interval
|
411
|
+
interval_incr = @immediate_kill_interval_increment
|
412
|
+
if @immediate_kill_timeout >= 0 &&
|
413
|
+
@immediate_kill_start_time <= now - @immediate_kill_timeout
|
414
|
+
# escalate to SIGKILL
|
415
|
+
signal = :KILL
|
416
|
+
else
|
417
|
+
signal = @immediate_kill_signal
|
418
|
+
end
|
419
|
+
|
420
|
+
else
|
421
|
+
signal = @graceful_kill_signal
|
422
|
+
interval = @graceful_kill_interval
|
423
|
+
interval_incr = @graceful_kill_interval_increment
|
424
|
+
end
|
425
|
+
|
426
|
+
begin
|
427
|
+
if ServerEngine.windows? && (signal == :KILL || signal == :SIGKILL)
|
428
|
+
system("taskkill /f /pid #{pid}")
|
429
|
+
else
|
430
|
+
Process.kill(signal, pid)
|
431
|
+
end
|
432
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
433
|
+
# assume that any errors mean the child process is dead
|
434
|
+
@pid = nil
|
435
|
+
return false
|
436
|
+
end
|
437
|
+
|
438
|
+
@next_kill_time = now + interval + interval_incr * @kill_count
|
439
|
+
@kill_count += 1
|
440
|
+
|
441
|
+
# expect next tick
|
442
|
+
return true
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
class TickThread < Thread
|
447
|
+
def initialize(auto_tick_interval, &tick)
|
448
|
+
@auto_tick_interval = auto_tick_interval
|
449
|
+
@tick = tick
|
450
|
+
super(&method(:main))
|
451
|
+
end
|
452
|
+
|
453
|
+
private
|
454
|
+
|
455
|
+
def main
|
456
|
+
while true
|
457
|
+
@tick.call(@auto_tick_interval)
|
458
|
+
end
|
459
|
+
nil
|
460
|
+
rescue AlreadyClosedError
|
461
|
+
nil
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
class Target
|
466
|
+
def initialize(pipe)
|
467
|
+
@pipe = pipe
|
468
|
+
end
|
469
|
+
|
470
|
+
attr_reader :pipe
|
471
|
+
|
472
|
+
def heartbeat!
|
473
|
+
@pipe.write HEARTBEAT_MESSAGE
|
474
|
+
end
|
475
|
+
|
476
|
+
def close
|
477
|
+
if @pipe
|
478
|
+
@pipe.close rescue nil
|
479
|
+
@pipe = nil
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
class HeartbeatThread < Thread
|
485
|
+
def initialize(heartbeat_interval, target, error_proc)
|
486
|
+
@heartbeat_interval = heartbeat_interval
|
487
|
+
@target = target
|
488
|
+
@error_proc = error_proc
|
489
|
+
super(&method(:main))
|
490
|
+
end
|
491
|
+
|
492
|
+
private
|
493
|
+
|
494
|
+
def main
|
495
|
+
while true
|
496
|
+
sleep @heartbeat_interval
|
497
|
+
@target.heartbeat!
|
498
|
+
end
|
499
|
+
nil
|
500
|
+
rescue
|
501
|
+
@error_proc.call(self)
|
502
|
+
nil
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
end
|
507
|
+
|
508
|
+
end
|