daemon_controller 1.2.0 → 3.0.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.
- checksums.yaml +5 -13
- data/LICENSE.txt +1 -2
- data/{README.markdown → README.md} +108 -142
- data/Rakefile +16 -574
- data/daemon_controller.gemspec +5 -4
- data/lib/daemon_controller/lock_file.rb +106 -105
- data/lib/daemon_controller/packaging.rb +10 -16
- data/lib/daemon_controller/spawn.rb +8 -7
- data/lib/daemon_controller/version.rb +10 -8
- data/lib/daemon_controller.rb +837 -851
- data/spec/daemon_controller_spec.rb +686 -409
- data/spec/echo_server.rb +147 -115
- data/spec/test_helper.rb +178 -105
- data/spec/unresponsive_daemon.rb +9 -9
- metadata +7 -21
- checksums.yaml.gz.asc +0 -12
- data/debian.template/changelog +0 -5
- data/debian.template/compat +0 -1
- data/debian.template/control.template +0 -15
- data/debian.template/copyright +0 -24
- data/debian.template/ruby-daemon-controller.install +0 -2
- data/debian.template/rules +0 -17
- data/debian.template/source/format +0 -1
- data/rpm/get_distro_id.py +0 -4
- data/rpm/rubygem-daemon_controller.spec.template +0 -102
- data/spec/run_echo_server +0 -9
- data.tar.gz.asc +0 -12
- metadata.gz.asc +0 -12
@@ -1,424 +1,701 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "test_helper"
|
4
|
+
require "daemon_controller"
|
5
|
+
require "benchmark"
|
6
|
+
require "socket"
|
7
|
+
require "tmpdir"
|
8
|
+
require "shellwords"
|
9
|
+
require "stringio"
|
10
|
+
require "logger"
|
5
11
|
|
6
12
|
describe DaemonController, "#start" do
|
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
|
-
|
13
|
+
include TestHelper
|
14
|
+
|
15
|
+
around :each do |example|
|
16
|
+
error = example.run
|
17
|
+
@controller.stop if @controller
|
18
|
+
print_logs(example) if error
|
19
|
+
end
|
20
|
+
|
21
|
+
it "works" do
|
22
|
+
new_controller
|
23
|
+
@controller.start
|
24
|
+
expect(ping_echo_server).to be(true)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises AlreadyStarted if the daemon is already running" do
|
28
|
+
new_controller
|
29
|
+
begin
|
30
|
+
expect(@controller).to receive(:daemon_is_running?).and_return(true)
|
31
|
+
expect { @controller.start }.to raise_error(DaemonController::AlreadyStarted)
|
32
|
+
ensure
|
33
|
+
@controller = nil # Don't invoke @controller.stop in after hook
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "deletes existing PID file before starting the daemon" do
|
38
|
+
write_file("spec/echo_server.pid", "1234")
|
39
|
+
new_controller
|
40
|
+
expect(@controller).to receive(:daemon_is_running?).and_return(false)
|
41
|
+
expect(@controller).to receive(:spawn_daemon).and_return(DaemonController::InternalCommandOkResult.new(1, ""))
|
42
|
+
expect(@controller).to receive(:pid_file_available?).and_return(true)
|
43
|
+
expect(@controller).to receive(:run_ping_command).at_least(:once).and_return(true)
|
44
|
+
@controller.start
|
45
|
+
expect(File.exist?("spec/echo_server.pid")).to be false
|
46
|
+
ensure
|
47
|
+
@controller = nil # Don't invoke @controller.stop in after hook
|
48
|
+
end
|
49
|
+
|
50
|
+
it "blocks until the daemon has written to its PID file" do
|
51
|
+
thread = WaitingThread.new do
|
52
|
+
sleep 0.15
|
53
|
+
write_file("spec/echo_server.pid", "1234")
|
54
|
+
end
|
55
|
+
new_controller
|
56
|
+
expect(@controller).to receive(:daemon_is_running?) { false }
|
57
|
+
expect(@controller).to receive(:spawn_daemon) {
|
58
|
+
thread.go!
|
59
|
+
DaemonController::InternalCommandOkResult.new(1, "")
|
60
|
+
}
|
61
|
+
expect(@controller).to receive(:run_ping_command).at_least(:once).and_return(true)
|
62
|
+
begin
|
63
|
+
result = Benchmark.measure do
|
64
|
+
@controller.start
|
65
|
+
end
|
66
|
+
expect(result.real).to be_between(0.15, 0.30)
|
67
|
+
ensure
|
68
|
+
thread.join
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
@controller = nil # Don't invoke @controller.stop in after hook
|
72
|
+
end
|
73
|
+
|
74
|
+
it "blocks until the daemon can be pinged" do
|
75
|
+
ping_ok = false
|
76
|
+
running = false
|
77
|
+
thread = WaitingThread.new do
|
78
|
+
sleep 0.15
|
79
|
+
ping_ok = true
|
80
|
+
end
|
81
|
+
new_controller
|
82
|
+
expect(@controller).to receive(:daemon_is_running?).at_least(:once) { running }
|
83
|
+
expect(@controller).to receive(:spawn_daemon) {
|
84
|
+
thread.go!
|
85
|
+
running = true
|
86
|
+
DaemonController::InternalCommandOkResult.new(1, "")
|
87
|
+
}
|
88
|
+
expect(@controller).to receive(:pid_file_available?).and_return(true)
|
89
|
+
expect(@controller).to receive(:run_ping_command).at_least(:once) { ping_ok }
|
90
|
+
begin
|
91
|
+
result = Benchmark.measure do
|
92
|
+
@controller.start
|
93
|
+
end
|
94
|
+
expect(result.real).to be_between(0.15, 0.30)
|
95
|
+
ensure
|
96
|
+
thread.join
|
97
|
+
end
|
98
|
+
ensure
|
99
|
+
@controller = nil # Don't invoke @controller.stop in after hook
|
100
|
+
end
|
101
|
+
|
102
|
+
it "works when the log file is not a regular file" do
|
103
|
+
new_controller(log_file: "/dev/stderr")
|
104
|
+
@controller.start
|
105
|
+
expect(ping_echo_server).to be(true)
|
106
|
+
end
|
107
|
+
|
108
|
+
context "if the daemon exits with an error" do
|
109
|
+
context "before forking" do
|
110
|
+
it "raises StartError" do
|
111
|
+
new_controller(start_command: "false")
|
112
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "makes outputs available in the exception" do
|
116
|
+
new_controller(start_command: "(echo hello world; false)")
|
117
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError, /hello world/)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "doesn't make outputs available if the log file is not a regular file" do
|
121
|
+
new_controller(start_command: "false", log_file: "/dev/stderr")
|
122
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError, /logs not available/)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "makes file logs available in the exception" do
|
126
|
+
new_controller(start_command: "(echo hello world; false)")
|
127
|
+
begin
|
128
|
+
@controller.start
|
129
|
+
rescue DaemonController::StartError => e
|
130
|
+
expect(e.message).to eq("hello world\n(exited with status 1)")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
specify "if file logs are available but empty, then the exception says so" do
|
135
|
+
new_controller(start_command: "exit 1")
|
136
|
+
begin
|
137
|
+
@controller.start
|
138
|
+
fail
|
139
|
+
rescue DaemonController::StartError => e
|
140
|
+
expect(e.message).to eq("(logs empty; exited with status 1)")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it "makes the exit signal available in the exception if the log file is a regular file" do
|
145
|
+
new_controller(crash_before_bind: true, crash_signal: "SIGXCPU", no_daemonize: true)
|
146
|
+
begin
|
147
|
+
@controller.start
|
148
|
+
fail
|
149
|
+
rescue DaemonController::StartError => e
|
150
|
+
expect(e.message).to include("crashing, as instructed")
|
151
|
+
expect(e.message).to include("SIGXCPU")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
it "makes the exit signal available in the exception if the log file is not a regular file" do
|
156
|
+
new_controller(crash_before_bind: true,
|
157
|
+
crash_signal: "SIGXCPU",
|
158
|
+
no_daemonize: true,
|
159
|
+
log_file: "/dev/stderr")
|
160
|
+
begin
|
161
|
+
@controller.start
|
162
|
+
fail
|
163
|
+
rescue DaemonController::StartError => e
|
164
|
+
expect(e.message).to include("logs not available")
|
165
|
+
expect(e.message).to include("SIGXCPU")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "after forking" do
|
171
|
+
it "raises StartError" do
|
172
|
+
new_controller(crash_before_bind: true)
|
173
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "makes outputs available in the exception" do
|
177
|
+
new_controller(log_message1: "hello world", crash_before_bind: true)
|
178
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError, /hello world/)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "doesn't make outputs available if the log file is not a regular file" do
|
182
|
+
new_controller(crash_before_bind: true, log_file: "/dev/stderr")
|
183
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError, "(logs not available)")
|
184
|
+
end
|
185
|
+
|
186
|
+
it "makes file logs available in the exception" do
|
187
|
+
new_controller(crash_before_bind: true)
|
188
|
+
expect { @controller.start }.to raise_error(DaemonController::StartError, /crashing, as instructed/)
|
189
|
+
end
|
190
|
+
|
191
|
+
specify "if the daemon didn't write a PID file, then the error is detected through log file inactivity" do
|
192
|
+
new_controller(log_message1: "hello",
|
193
|
+
log_message2: "world",
|
194
|
+
no_write_pid_file: true,
|
195
|
+
log_file_activity_timeout: 0.5)
|
196
|
+
begin
|
197
|
+
@controller.start
|
198
|
+
fail
|
199
|
+
rescue DaemonController::StartTimeout => e
|
200
|
+
expect(e.message).to include("hello")
|
201
|
+
expect(e.message).to include("world")
|
202
|
+
expect(e.message).to include("(timed out)")
|
203
|
+
ensure
|
204
|
+
# Kill echo_server without PID file
|
205
|
+
kill_and_wait_echo_server
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
context "if the daemon doesn't start in time" do
|
212
|
+
it "raises StartTimeout" do
|
213
|
+
start_timeout = exec_is_slow? ? 4 : 0.5
|
214
|
+
min_start_timeout = 0.5
|
215
|
+
max_start_timeout = exec_is_slow? ? 6 : 1
|
216
|
+
|
217
|
+
new_controller(start_command: "sleep 10", start_timeout: start_timeout)
|
218
|
+
start_time = monotonic_time
|
219
|
+
end_time = nil
|
220
|
+
expect(@controller).to receive(:daemonization_timed_out) { end_time = monotonic_time }
|
221
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
222
|
+
expect(end_time - start_time).to be_between(min_start_timeout, max_start_timeout)
|
223
|
+
end
|
224
|
+
|
225
|
+
it "doesn't terminate the fork if the daemon forked right before we perform termination" do
|
226
|
+
new_controller(log_message2: "hello world",
|
227
|
+
wait1: 0.5,
|
228
|
+
start_timeout: 0.1)
|
229
|
+
expect(@controller).to receive(:daemonization_timed_out) { |p|
|
230
|
+
wait_until_pid_file_available
|
231
|
+
}
|
232
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
233
|
+
expect(@controller).to be_running
|
234
|
+
end
|
235
|
+
|
236
|
+
context "if the daemon hasn't forked yet" do
|
237
|
+
it "terminates the daemon gracefully" do
|
238
|
+
new_controller(start_command: "./spec/run_in_mri_ruby unresponsive_daemon.rb",
|
239
|
+
start_timeout: 0.1)
|
240
|
+
|
241
|
+
pid = nil
|
242
|
+
expect(@controller).to receive(:daemonization_timed_out) { |p|
|
243
|
+
pid = p
|
244
|
+
wait_until_pid_file_available
|
245
|
+
}
|
246
|
+
|
247
|
+
allow(Process).to receive(:kill).and_call_original
|
248
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /logs empty/)
|
249
|
+
expect(Process).to have_received(:kill).with("SIGTERM", pid).once
|
250
|
+
expect(Process).not_to have_received(:kill).with("SIGKILL", pid)
|
251
|
+
ensure
|
252
|
+
@controller = nil
|
253
|
+
end
|
254
|
+
|
255
|
+
it "terminates the daemon forcefully if it doesn't gracefully terminate in time" do
|
256
|
+
new_controller(
|
257
|
+
wait2: 10,
|
258
|
+
start_timeout: 0.1,
|
259
|
+
start_abort_timeout: 0.1,
|
260
|
+
ignore_sigterm: true,
|
261
|
+
no_daemonize: true
|
262
|
+
)
|
263
|
+
|
264
|
+
pid = nil
|
265
|
+
expect(@controller).to receive(:daemonization_timed_out) { |p|
|
266
|
+
pid = p
|
267
|
+
wait_until_pid_file_available
|
268
|
+
}
|
269
|
+
|
270
|
+
allow(Process).to receive(:kill).and_call_original
|
271
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
272
|
+
expect(Process).to have_received(:kill).with("SIGTERM", pid).once
|
273
|
+
expect(Process).to have_received(:kill).with("SIGKILL", pid).once
|
274
|
+
end
|
275
|
+
|
276
|
+
it "deletes the PID file" do
|
277
|
+
new_controller(wait2: 10,
|
278
|
+
start_timeout: 0.1,
|
279
|
+
no_daemonize: true)
|
280
|
+
expect(@controller).to receive(:daemonization_timed_out) {
|
281
|
+
wait_until_pid_file_available
|
282
|
+
}
|
283
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
284
|
+
expect(File.exist?("spec/echo_server.pid")).to be(false)
|
285
|
+
end
|
286
|
+
|
287
|
+
it "reports logs written to standard I/O channels" do
|
288
|
+
new_controller(log_message2: "hello world",
|
289
|
+
wait2: 10,
|
290
|
+
start_timeout: 0.1,
|
291
|
+
no_daemonize: true)
|
292
|
+
expect(@controller).to receive(:daemonization_timed_out) { |p|
|
293
|
+
wait_until_pid_file_available
|
294
|
+
}
|
295
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /hello world/)
|
296
|
+
end
|
297
|
+
|
298
|
+
it "reports logs written to the log file" do
|
299
|
+
new_controller(log_message2: "hello world",
|
300
|
+
wait2: 10,
|
301
|
+
start_timeout: 0.1,
|
302
|
+
no_daemonize: true)
|
303
|
+
expect(@controller).to receive(:daemonization_timed_out) {
|
304
|
+
wait_until_pid_file_available
|
305
|
+
}
|
306
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /hello world/)
|
307
|
+
end
|
308
|
+
|
309
|
+
specify "if there are no logs, then the error says so" do
|
310
|
+
new_controller(wait2: 10,
|
311
|
+
start_timeout: 0.1,
|
312
|
+
no_daemonize: true)
|
313
|
+
expect(@controller).to receive(:daemonization_timed_out) {
|
314
|
+
wait_until_pid_file_available
|
315
|
+
}
|
316
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /\(logs empty; timed out\)/)
|
317
|
+
end
|
318
|
+
|
319
|
+
specify "if logs cannot be captured, then the error says so" do
|
320
|
+
new_controller(wait2: 10,
|
321
|
+
start_timeout: 0.1,
|
322
|
+
log_file: "/dev/stderr",
|
323
|
+
no_daemonize: true)
|
324
|
+
expect(@controller).to receive(:daemonization_timed_out) {
|
325
|
+
wait_until_pid_file_available
|
326
|
+
}
|
327
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /\(logs not available; timed out\)/)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
context "if the daemon already forked" do
|
332
|
+
it "terminates the daemon gracefully" do
|
333
|
+
new_controller(wait2: 10)
|
334
|
+
|
335
|
+
expect(@controller).to receive(:daemon_spawned) {
|
336
|
+
wait_until_pid_file_available
|
337
|
+
raise Timeout::Error
|
338
|
+
}
|
339
|
+
|
340
|
+
pid = nil
|
341
|
+
expect(@controller).to receive(:start_timed_out) { |p| pid = p }
|
342
|
+
|
343
|
+
allow(Process).to receive(:kill).and_call_original
|
344
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
345
|
+
expect(Process).to have_received(:kill).with("SIGTERM", pid).once
|
346
|
+
expect(Process).not_to have_received(:kill).with("SIGKILL", pid)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "terminates the daemon forcefully if it doesn't gracefully terminate in time" do
|
350
|
+
new_controller(wait2: 10,
|
351
|
+
start_abort_timeout: 0.5,
|
352
|
+
ignore_sigterm: true)
|
353
|
+
|
354
|
+
expect(@controller).to receive(:daemon_spawned) {
|
355
|
+
wait_until_pid_file_available
|
356
|
+
raise Timeout::Error
|
357
|
+
}
|
358
|
+
|
359
|
+
pid = nil
|
360
|
+
expect(@controller).to receive(:start_timed_out) { |p| pid = p }
|
361
|
+
|
362
|
+
allow(Process).to receive(:kill).and_call_original
|
363
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
364
|
+
expect(Process).to have_received(:kill).with("SIGTERM", pid).once
|
365
|
+
expect(Process).to have_received(:kill).with("SIGKILL", pid).once
|
366
|
+
end
|
367
|
+
|
368
|
+
it "deletes the PID file" do
|
369
|
+
new_controller(wait2: 10)
|
370
|
+
expect(@controller).to receive(:daemon_spawned) {
|
371
|
+
wait_until_pid_file_available
|
372
|
+
raise Timeout::Error
|
373
|
+
}
|
374
|
+
expect(@controller).to receive(:start_timed_out)
|
375
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout)
|
376
|
+
expect(File.exist?("spec/echo_server.pid")).to be(false)
|
377
|
+
end
|
378
|
+
|
379
|
+
it "reports logs written to standard I/O channels" do
|
380
|
+
new_controller(log_message1: "hello world", wait2: 10)
|
381
|
+
expect(@controller).to receive(:daemon_spawned) {
|
382
|
+
wait_until_pid_file_available
|
383
|
+
raise Timeout::Error
|
384
|
+
}
|
385
|
+
expect(@controller).to receive(:start_timed_out)
|
386
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /hello world/)
|
387
|
+
end
|
388
|
+
|
389
|
+
it "reports logs written to the log file" do
|
390
|
+
new_controller(log_message2: "hello world", wait2: 10)
|
391
|
+
expect(@controller).to receive(:daemon_spawned) {
|
392
|
+
wait_until_pid_file_available
|
393
|
+
raise Timeout::Error
|
394
|
+
}
|
395
|
+
expect(@controller).to receive(:start_timed_out)
|
396
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /hello world/)
|
397
|
+
end
|
398
|
+
|
399
|
+
specify "if there are no logs, then the error says so" do
|
400
|
+
new_controller(wait2: 10)
|
401
|
+
expect(@controller).to receive(:daemon_spawned) {
|
402
|
+
wait_until_pid_file_available
|
403
|
+
raise Timeout::Error
|
404
|
+
}
|
405
|
+
expect(@controller).to receive(:start_timed_out)
|
406
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /\(logs empty; timed out\)/)
|
407
|
+
end
|
408
|
+
|
409
|
+
specify "if logs cannot be captured, then the error says so" do
|
410
|
+
new_controller(log_message2: "hello world", wait2: 10, log_file: "/dev/stderr")
|
411
|
+
expect(@controller).to receive(:daemon_spawned) {
|
412
|
+
wait_until_pid_file_available
|
413
|
+
raise Timeout::Error
|
414
|
+
}
|
415
|
+
expect(@controller).to receive(:start_timed_out)
|
416
|
+
expect { @controller.start }.to raise_error(DaemonController::StartTimeout, /\(logs not available; timed out\)/)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
specify "the start command may be a Proc" do
|
422
|
+
called = true
|
423
|
+
new_controller(start_command: lambda {
|
424
|
+
called = true
|
425
|
+
@start_command
|
426
|
+
})
|
427
|
+
@controller.start
|
428
|
+
expect(called).to be true
|
429
|
+
end
|
430
|
+
|
431
|
+
specify "if the start command is a Proc then it is called after before_start" do
|
432
|
+
log = []
|
433
|
+
new_controller(
|
434
|
+
start_command: lambda {
|
435
|
+
log << "start_command"
|
436
|
+
@start_command
|
437
|
+
},
|
438
|
+
before_start: lambda { log << "before_start" }
|
439
|
+
)
|
440
|
+
@controller.start
|
441
|
+
expect(log).to eq(["before_start", "start_command"])
|
442
|
+
end
|
443
|
+
|
444
|
+
it "keeps the file descriptors in 'keep_ios' open" do
|
445
|
+
a, b = IO.pipe
|
446
|
+
begin
|
447
|
+
new_controller(keep_ios: [b])
|
448
|
+
@controller.start
|
449
|
+
b.close
|
450
|
+
expect(select([a], nil, nil, 0)).to be_nil
|
451
|
+
ensure
|
452
|
+
a.close if !a.closed?
|
453
|
+
b.close if !b.closed?
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
it "performs the daemonization on behalf of the daemon if 'daemonize_for_me' is set" do
|
458
|
+
new_controller(no_daemonize: true, daemonize_for_me: true)
|
459
|
+
@controller.start
|
460
|
+
expect(ping_echo_server).to be true
|
461
|
+
end
|
462
|
+
|
463
|
+
it "passes environment variables" do
|
464
|
+
new_controller(env: {"ENV_FILE" => "spec/env_file.tmp"})
|
465
|
+
File.unlink("spec/env_file.tmp") if File.exist?("spec/env_file.tmp")
|
466
|
+
@controller.start
|
467
|
+
expect(File.exist?("spec/env_file.tmp")).to be true
|
468
|
+
end
|
245
469
|
end
|
246
470
|
|
247
471
|
describe DaemonController, "#stop" do
|
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
|
-
|
472
|
+
include TestHelper
|
473
|
+
|
474
|
+
before :each do
|
475
|
+
new_controller
|
476
|
+
end
|
477
|
+
|
478
|
+
it "raises no exception if the daemon is not running" do
|
479
|
+
@controller.stop
|
480
|
+
end
|
481
|
+
|
482
|
+
it "waits until the daemon is no longer running" do
|
483
|
+
new_controller(stop_time: 0.3)
|
484
|
+
@controller.start
|
485
|
+
begin
|
486
|
+
result = Benchmark.measure do
|
487
|
+
@controller.stop
|
488
|
+
end
|
489
|
+
expect(@controller).not_to be_running
|
490
|
+
expect(result.real).to be_between(0.3, 3)
|
491
|
+
ensure
|
492
|
+
new_controller.stop
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
context "if the daemon does not stop in time" do
|
497
|
+
before :each do
|
498
|
+
new_controller(stop_time: 0.3, stop_timeout: 0.1)
|
499
|
+
@controller.start
|
500
|
+
end
|
501
|
+
|
502
|
+
after :each do
|
503
|
+
new_controller.stop
|
504
|
+
end
|
505
|
+
|
506
|
+
it "raises StopTimeout" do
|
507
|
+
expect { @controller.stop }.to raise_error(DaemonController::StopTimeout)
|
508
|
+
end
|
509
|
+
|
510
|
+
it "forcefully terminates the daemon and raises StopTimeout" do
|
511
|
+
pid = @controller.pid
|
512
|
+
allow(Process).to receive(:kill).and_call_original
|
513
|
+
expect { @controller.stop }.to raise_error(DaemonController::StopTimeout)
|
514
|
+
expect(Process).to have_received(:kill).with("SIGKILL", pid).once
|
515
|
+
expect(File.exist?("spec/echo_server.pid")).to be(false)
|
516
|
+
end
|
517
|
+
|
518
|
+
it "deletes the PID file" do
|
519
|
+
expect { @controller.stop }.to raise_error(DaemonController::StopTimeout)
|
520
|
+
expect(File.exist?("spec/echo_server.pid")).to be(false)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
describe "if stop command was given" do
|
525
|
+
it "raises StopError if the stop command exits with an error" do
|
526
|
+
new_controller(stop_command: "(echo hello world; false)")
|
527
|
+
@controller.start
|
528
|
+
begin
|
529
|
+
expect { @controller.stop }.to raise_error(DaemonController::StopError)
|
530
|
+
ensure
|
531
|
+
new_controller.stop
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
it "makes the stop command's error message available in the exception" do
|
536
|
+
new_controller(stop_command: "(echo hello world; false)")
|
537
|
+
begin
|
538
|
+
@controller.start
|
539
|
+
@controller.stop
|
540
|
+
fail
|
541
|
+
rescue DaemonController::StopError => e
|
542
|
+
expect(e.message).to include("hello world")
|
543
|
+
expect(e.message).to include("(exited with status 1)")
|
544
|
+
ensure
|
545
|
+
new_controller.stop
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
it "calls the stop command if the PID file is invalid and :dont_stop_if_pid_file_invalid is not set" do
|
550
|
+
Dir.mktmpdir do |tmpdir|
|
551
|
+
new_controller(stop_command: "touch #{Shellwords.escape tmpdir}/stopped")
|
552
|
+
@controller.start
|
553
|
+
begin
|
554
|
+
File.open("spec/echo_server.pid", "w").close
|
555
|
+
@controller.stop
|
556
|
+
expect(File.exist?("#{tmpdir}/stopped")).to be_truthy
|
557
|
+
ensure
|
558
|
+
# Kill echo_server without PID file
|
559
|
+
kill_and_wait_echo_server
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
it "does not call the stop command if the PID file is invalid and :dont_stop_if_pid_file_invalid is set" do
|
565
|
+
Dir.mktmpdir do |tmpdir|
|
566
|
+
File.open("spec/echo_server.pid", "w").close
|
567
|
+
new_controller(stop_command: "touch #{Shellwords.escape tmpdir}/stopped",
|
568
|
+
dont_stop_if_pid_file_invalid: true)
|
569
|
+
@controller.stop
|
570
|
+
expect(File.exist?("#{tmpdir}/stopped")).to be_falsey
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|
300
574
|
end
|
301
575
|
|
302
576
|
describe DaemonController, "#restart" do
|
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
|
-
|
577
|
+
include TestHelper
|
578
|
+
|
579
|
+
before :each do
|
580
|
+
new_controller
|
581
|
+
end
|
582
|
+
|
583
|
+
it "raises no exception if the daemon is not running" do
|
584
|
+
@controller.restart
|
585
|
+
end
|
586
|
+
|
587
|
+
describe "with no restart command" do
|
588
|
+
it "restart the daemon using stop and start" do
|
589
|
+
expect(@controller).to receive(:stop)
|
590
|
+
expect(@controller).to receive(:start)
|
591
|
+
@controller.restart
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
describe "with a restart_command" do
|
596
|
+
it "restarts the daemon using the restart_command" do
|
597
|
+
stop_cmd = "echo 'hello world'"
|
598
|
+
new_controller restart_command: stop_cmd
|
599
|
+
|
600
|
+
expect(@controller).to receive(:run_command).with(stop_cmd)
|
601
|
+
@controller.restart
|
602
|
+
end
|
603
|
+
end
|
330
604
|
end
|
331
605
|
|
332
606
|
describe DaemonController, "#connect" do
|
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
|
-
|
607
|
+
include TestHelper
|
608
|
+
|
609
|
+
before :each do
|
610
|
+
new_controller
|
611
|
+
end
|
612
|
+
|
613
|
+
it "starts the daemon if it isn't already running" do
|
614
|
+
socket = @controller.connect do
|
615
|
+
TCPSocket.new("localhost", 3230)
|
616
|
+
end
|
617
|
+
socket.close
|
618
|
+
@controller.stop
|
619
|
+
end
|
620
|
+
|
621
|
+
it "connects to the existing daemon if it's already running" do
|
622
|
+
@controller.start
|
623
|
+
begin
|
624
|
+
socket = @controller.connect do
|
625
|
+
TCPSocket.new("localhost", 3230)
|
626
|
+
end
|
627
|
+
socket.close
|
628
|
+
ensure
|
629
|
+
@controller.stop
|
630
|
+
end
|
631
|
+
end
|
358
632
|
end
|
359
633
|
|
360
634
|
describe DaemonController do
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
635
|
+
include TestHelper
|
636
|
+
|
637
|
+
after :each do
|
638
|
+
@server.close if @server && !@server.closed?
|
639
|
+
begin
|
640
|
+
File.unlink("spec/foo.sock")
|
641
|
+
rescue
|
642
|
+
nil
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
specify "if the ping command is a block that raises Errno::ECONNREFUSED, then that's " \
|
647
|
+
"an indication that the daemon cannot be connected to" do
|
648
|
+
new_controller(ping_command: lambda do
|
649
|
+
raise Errno::ECONNREFUSED, "dummy"
|
650
|
+
end)
|
651
|
+
expect(@controller.send(:run_ping_command)).to be false
|
652
|
+
end
|
653
|
+
|
654
|
+
specify "if the ping command is a block that returns an object that responds to #close, " \
|
655
|
+
"then the close method will be called on that object" do
|
656
|
+
@server = TCPServer.new("localhost", 8278)
|
657
|
+
socket = nil
|
658
|
+
new_controller(ping_command: lambda do
|
659
|
+
socket = TCPSocket.new("localhost", 8278)
|
660
|
+
end)
|
661
|
+
@controller.send(:run_ping_command)
|
662
|
+
expect(socket).to be_closed
|
663
|
+
end
|
664
|
+
|
665
|
+
specify "if the ping command is a block that returns an object that responds to #close, " \
|
666
|
+
"and #close raises an exception, then that exception is ignored" do
|
667
|
+
@server = TCPServer.new("localhost", 8278)
|
668
|
+
o = Object.new
|
669
|
+
expect(o).to receive(:close) { raise StandardError, "foo" }
|
670
|
+
new_controller(ping_command: lambda do
|
671
|
+
o
|
672
|
+
end)
|
673
|
+
expect { @controller.send(:run_ping_command) }.not_to raise_error
|
674
|
+
end
|
675
|
+
|
676
|
+
specify "the ping command may be [:tcp, hostname, port]" do
|
677
|
+
new_controller(ping_command: [:tcp, "127.0.0.1", 8278])
|
678
|
+
expect(@controller.send(:run_ping_command)).to be false
|
679
|
+
|
680
|
+
@server = TCPServer.new("127.0.0.1", 8278)
|
681
|
+
expect(@controller.send(:run_ping_command)).to be true
|
682
|
+
end
|
683
|
+
|
684
|
+
if DaemonController.can_ping_unix_sockets?
|
685
|
+
specify "the ping command may be [:unix, filename]" do
|
686
|
+
new_controller(ping_command: [:unix, "spec/foo.sock"])
|
687
|
+
expect(@controller.send(:run_ping_command)).to be false
|
688
|
+
|
689
|
+
@server = UNIXServer.new("spec/foo.sock")
|
690
|
+
expect(@controller.send(:run_ping_command)).to be true
|
691
|
+
end
|
692
|
+
else
|
693
|
+
specify "a ping command of type [:unix, filename] is not supported on this Ruby implementation" do
|
694
|
+
new_controller(ping_command: [:unix, "spec/foo.sock"])
|
695
|
+
@server = UNIXServer.new("spec/foo.sock")
|
696
|
+
expect { @controller.send(:run_ping_command) }.to raise_error(
|
697
|
+
"Pinging Unix domain sockets is not supported on this Ruby implementation"
|
698
|
+
)
|
699
|
+
end
|
700
|
+
end
|
424
701
|
end
|