siding 0.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 +7 -0
- data/CLAUDE.md +248 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +51 -0
- data/exe/siding +5 -0
- data/lib/siding/boot_component.rb +18 -0
- data/lib/siding/cli.rb +459 -0
- data/lib/siding/client.rb +417 -0
- data/lib/siding/error.rb +5 -0
- data/lib/siding/invocation.rb +35 -0
- data/lib/siding/life_cycle.rb +114 -0
- data/lib/siding/load_manifest.rb +475 -0
- data/lib/siding/logger.rb +89 -0
- data/lib/siding/platform.rb +37 -0
- data/lib/siding/project_key.rb +64 -0
- data/lib/siding/protocol.rb +154 -0
- data/lib/siding/restarter.rb +205 -0
- data/lib/siding/runtime.rb +165 -0
- data/lib/siding/server.rb +398 -0
- data/lib/siding/staleness.rb +191 -0
- data/lib/siding/version.rb +5 -0
- data/lib/siding/watch.rb +115 -0
- data/lib/siding/worker.rb +266 -0
- data/lib/siding.rb +27 -0
- metadata +97 -0
data/lib/siding/cli.rb
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "version"
|
|
7
|
+
require_relative "platform"
|
|
8
|
+
require_relative "runtime"
|
|
9
|
+
require_relative "protocol"
|
|
10
|
+
require_relative "staleness"
|
|
11
|
+
|
|
12
|
+
module Siding
|
|
13
|
+
class CLI
|
|
14
|
+
MANAGEMENT_COMMANDS = %w[start status stop restart doctor init version help --help -h].freeze
|
|
15
|
+
ACCELERATED_EXECUTABLES = %w[rails rake rspec test].freeze
|
|
16
|
+
OUT_OF_SCOPE_RAILS_COMMANDS = %w[dev:cache].freeze
|
|
17
|
+
SERVER_COMMANDS = %w[server s].freeze
|
|
18
|
+
DAEMON_FLAGS = %w[-d --daemon].freeze
|
|
19
|
+
|
|
20
|
+
REQUEST_TIMEOUT = 5
|
|
21
|
+
BOOT_LOG_LINES = 20
|
|
22
|
+
|
|
23
|
+
BINSTUBS = %w[rails rake rspec].freeze
|
|
24
|
+
BINSTUB_MARKER = "generated by siding"
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def management?(name) = MANAGEMENT_COMMANDS.include?(name)
|
|
28
|
+
|
|
29
|
+
def accelerated?(argv)
|
|
30
|
+
executable = argv.first
|
|
31
|
+
return false unless ACCELERATED_EXECUTABLES.include?(executable)
|
|
32
|
+
return rails_accelerated?(argv) if executable == "rails"
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rails_accelerated?(argv)
|
|
38
|
+
command = argv[1]
|
|
39
|
+
return false if OUT_OF_SCOPE_RAILS_COMMANDS.include?(command)
|
|
40
|
+
return false if SERVER_COMMANDS.include?(command) && argv.any? { DAEMON_FLAGS.include?(_1) }
|
|
41
|
+
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_reader :argv, :client
|
|
47
|
+
|
|
48
|
+
def initialize(argv, client)
|
|
49
|
+
@argv = argv
|
|
50
|
+
@client = client
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def run
|
|
54
|
+
command = argv.first
|
|
55
|
+
ret = if ["help", "--help", "-h", nil].include?(command)
|
|
56
|
+
help
|
|
57
|
+
elsif MANAGEMENT_COMMANDS.include?(command)
|
|
58
|
+
send(command)
|
|
59
|
+
else
|
|
60
|
+
unimplemented(command)
|
|
61
|
+
end
|
|
62
|
+
ret.is_a?(Integer) ? ret : 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def logger = client.logger
|
|
68
|
+
|
|
69
|
+
def status
|
|
70
|
+
runtime = client.runtime
|
|
71
|
+
return report("no Rails application found here", warm: false) if runtime.nil?
|
|
72
|
+
|
|
73
|
+
info = runtime.live_server_info
|
|
74
|
+
if info.nil?
|
|
75
|
+
runtime.discard_records if runtime.server_info
|
|
76
|
+
return report("no warm application for #{project_label}", warm: false)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
report_status(info, request_status(runtime))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def stop
|
|
83
|
+
runtime = client.runtime
|
|
84
|
+
return unless runtime && File.directory?(runtime.dir)
|
|
85
|
+
|
|
86
|
+
pid = runtime.server_pid
|
|
87
|
+
if pid.nil?
|
|
88
|
+
runtime.discard_records
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
request_stop(runtime)
|
|
93
|
+
terminate(pid)
|
|
94
|
+
runtime.discard_records
|
|
95
|
+
logger.debug("stopped server pid #{pid}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def start
|
|
99
|
+
outcome = client.warm_up
|
|
100
|
+
return report_start_failure unless outcome
|
|
101
|
+
|
|
102
|
+
state = outcome == :already_warm ? "is already warm" : "is warm"
|
|
103
|
+
logger.notice("#{project_label} #{state}#{server_suffix}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def server_suffix
|
|
107
|
+
pid = client.runtime&.server_pid
|
|
108
|
+
pid ? " -- pid #{pid}" : ""
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def report_start_failure
|
|
112
|
+
reason = client.unusable_reason
|
|
113
|
+
if reason
|
|
114
|
+
logger.notice("cannot boot a warm application for #{project_label}: #{reason}")
|
|
115
|
+
else
|
|
116
|
+
logger.notice("#{project_label} did not boot within its bound; its output is in #{client.runtime.boot_log_path}")
|
|
117
|
+
end
|
|
118
|
+
1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def restart
|
|
122
|
+
stop
|
|
123
|
+
return if client.warm_up
|
|
124
|
+
|
|
125
|
+
logger.notice("stopped, but nothing was booted -- the next command will boot one")
|
|
126
|
+
1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def version
|
|
130
|
+
logger.notice("siding #{Siding::VERSION}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def help
|
|
134
|
+
logger.notice(<<~TEXT.chomp)
|
|
135
|
+
siding #{Siding::VERSION} -- a Rails application preloader that never serves stale code
|
|
136
|
+
|
|
137
|
+
Usage:
|
|
138
|
+
siding <command> [args...] run a command against the warm application
|
|
139
|
+
siding start boot a warm application without running a command
|
|
140
|
+
siding status report whether a warm application exists
|
|
141
|
+
siding stop stop everything belonging to this project
|
|
142
|
+
siding restart stop, then boot fresh
|
|
143
|
+
siding doctor explain why an invocation was or was not accelerated
|
|
144
|
+
siding init generate opt-in binstubs
|
|
145
|
+
TEXT
|
|
146
|
+
0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def doctor
|
|
150
|
+
lines = ["siding #{Siding::VERSION} -- #{project_label}"]
|
|
151
|
+
lines.concat(platform_lines)
|
|
152
|
+
lines.concat(project_lines)
|
|
153
|
+
lines.concat(runtime_lines)
|
|
154
|
+
lines.concat(acceleration_lines)
|
|
155
|
+
lines.concat(server_lines)
|
|
156
|
+
lines.concat(boot_log_lines)
|
|
157
|
+
logger.notice(lines.join("\n"))
|
|
158
|
+
acceleration_available? ? 0 : 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def platform_lines
|
|
162
|
+
supported = Platform.supported?
|
|
163
|
+
lines = [" platform #{Platform.description}#{supported ? '' : ' -- unsupported'}"]
|
|
164
|
+
lines << " #{Platform.unsupported_reason}" unless supported
|
|
165
|
+
lines
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def project_lines
|
|
169
|
+
root = client.app_root
|
|
170
|
+
return [" project no Rails application found above #{client.cwd}"] if root.nil?
|
|
171
|
+
|
|
172
|
+
[" project #{root}",
|
|
173
|
+
" framework #{resolved_framework_version(root)}",
|
|
174
|
+
" ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})"]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def resolved_framework_version(root)
|
|
178
|
+
lock = File.join(root, "Gemfile.lock")
|
|
179
|
+
return "unknown (no Gemfile.lock; run `bundle install`)" unless File.file?(lock)
|
|
180
|
+
|
|
181
|
+
match = File.read(lock).match(/^\s{4}railties \(([^)]+)\)$/)
|
|
182
|
+
match ? "Rails #{match[1]} (from Gemfile.lock)" : "unknown (railties not in Gemfile.lock)"
|
|
183
|
+
rescue SystemCallError => e
|
|
184
|
+
"unknown (#{e.message})"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def runtime_lines
|
|
188
|
+
runtime = client.runtime
|
|
189
|
+
return [] if runtime.nil?
|
|
190
|
+
|
|
191
|
+
reason = runtime.unavailable_reason
|
|
192
|
+
lines = [" runtime #{runtime.dir}"]
|
|
193
|
+
lines << if reason
|
|
194
|
+
" unusable: #{reason}"
|
|
195
|
+
else
|
|
196
|
+
" #{directory_mode(runtime.dir)}, writable"
|
|
197
|
+
end
|
|
198
|
+
lines.concat(leftover_lines(runtime))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def directory_mode(path)
|
|
202
|
+
"mode #{format('%04o', File.stat(path).mode & 0o7777)}"
|
|
203
|
+
rescue SystemCallError
|
|
204
|
+
"mode unknown"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def leftover_lines(runtime)
|
|
208
|
+
return [] unless File.directory?(runtime.dir)
|
|
209
|
+
|
|
210
|
+
info = runtime.server_info
|
|
211
|
+
return [] if info.nil?
|
|
212
|
+
return [] unless runtime.live_server_info.nil?
|
|
213
|
+
|
|
214
|
+
[" leftover record naming pid #{info['pid']} (not running; the next " \
|
|
215
|
+
"invocation clears it)"]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def acceleration_lines
|
|
219
|
+
reason = client.unusable_reason
|
|
220
|
+
lines = [reason ? " active no -- #{reason}" : " active yes"]
|
|
221
|
+
lines << " commands accelerated: #{ACCELERATED_EXECUTABLES.join(', ')}"
|
|
222
|
+
lines << " passed through: rails #{OUT_OF_SCOPE_RAILS_COMMANDS.join(', rails ')}, " \
|
|
223
|
+
"rails server -d, and everything else"
|
|
224
|
+
lines
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def acceleration_available? = client.unusable_reason.nil?
|
|
228
|
+
|
|
229
|
+
def server_lines
|
|
230
|
+
runtime = client.runtime
|
|
231
|
+
return [] if runtime.nil?
|
|
232
|
+
|
|
233
|
+
info = runtime.live_server_info
|
|
234
|
+
return [" warm nothing running"] + recorded_event_lines(runtime) if info.nil?
|
|
235
|
+
|
|
236
|
+
report = request_status(runtime)
|
|
237
|
+
if report.nil?
|
|
238
|
+
return [" warm pid #{info['pid']}, not answering yet"] + recorded_event_lines(runtime)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
[" warm pid #{info['pid']}, booted #{format_time(info['booted_at'])}" \
|
|
242
|
+
"#{format_boot_duration(report['boot_seconds'] || info['boot_seconds'])}",
|
|
243
|
+
" revision #{report['revision_label'] || info['revision_label']}",
|
|
244
|
+
" served #{format_served(report)}",
|
|
245
|
+
" watch #{report['watch'] || 'unknown'}"] + event_lines(Array(report["events"]))
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def recorded_event_lines(runtime)
|
|
249
|
+
events = Staleness::Events.new(path: runtime.events_path).to_a
|
|
250
|
+
.reject { |event| event.resolution == "fresh" }
|
|
251
|
+
.last(10).map(&:to_h)
|
|
252
|
+
events.empty? ? [] : event_lines(events)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def boot_log_lines
|
|
256
|
+
runtime = client.runtime
|
|
257
|
+
return [] if runtime.nil? || !runtime.live_server_info.nil?
|
|
258
|
+
|
|
259
|
+
path = runtime.boot_log_path
|
|
260
|
+
tail = File.file?(path) ? File.read(path).lines.last(BOOT_LOG_LINES) : []
|
|
261
|
+
return [] if tail.empty?
|
|
262
|
+
|
|
263
|
+
[" last boot left this behind in #{path}:"] +
|
|
264
|
+
tail.map { |line| " #{line.chomp}" }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def init
|
|
268
|
+
root = client.app_root
|
|
269
|
+
return report("no Rails application found above #{client.cwd}", warm: false) if root.nil?
|
|
270
|
+
|
|
271
|
+
written, skipped = write_binstubs(root)
|
|
272
|
+
logger.notice(init_report(root, written, skipped).join("\n"))
|
|
273
|
+
0
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def write_binstubs(root)
|
|
277
|
+
directory = File.join(root, "bin")
|
|
278
|
+
FileUtils.mkdir_p(directory)
|
|
279
|
+
written = []
|
|
280
|
+
skipped = []
|
|
281
|
+
|
|
282
|
+
BINSTUBS.each do |name|
|
|
283
|
+
path = File.join(directory, name)
|
|
284
|
+
# Never overwritten. `bin/rails` in a Rails application is generated, sometimes edited, and
|
|
285
|
+
# always committed -- replacing one without asking is precisely the "what did this put in my
|
|
286
|
+
# repository?" problem this command exists to avoid.
|
|
287
|
+
next skipped << path if File.exist?(path) && !siding_binstub?(path)
|
|
288
|
+
|
|
289
|
+
File.write(path, binstub_source(name))
|
|
290
|
+
File.chmod(0o755, path)
|
|
291
|
+
written << path
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
[written, skipped]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def siding_binstub?(path)
|
|
298
|
+
File.read(path).include?(BINSTUB_MARKER)
|
|
299
|
+
rescue SystemCallError
|
|
300
|
+
false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def binstub_source(name)
|
|
304
|
+
<<~RUBY
|
|
305
|
+
#!/usr/bin/env ruby
|
|
306
|
+
# frozen_string_literal: true
|
|
307
|
+
# #{BINSTUB_MARKER}
|
|
308
|
+
#
|
|
309
|
+
# Runs `#{name}` against the warm application when siding can accelerate it, and exactly as
|
|
310
|
+
# it would have run otherwise. Delete this file to remove it; nothing else refers to it.
|
|
311
|
+
exec("siding", "#{name}", *ARGV)
|
|
312
|
+
RUBY
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def init_report(root, written, skipped)
|
|
316
|
+
lines = ["siding #{Siding::VERSION} -- #{File.basename(root)}"]
|
|
317
|
+
lines << (written.empty? ? " wrote nothing" : " wrote:")
|
|
318
|
+
lines.concat(written.map { |path| " #{relative_to(path, root)}" })
|
|
319
|
+
unless skipped.empty?
|
|
320
|
+
lines << " left alone (not written by siding -- remove them yourself to replace):"
|
|
321
|
+
lines.concat(skipped.map { |path| " #{relative_to(path, root)}" })
|
|
322
|
+
end
|
|
323
|
+
lines << " siding <command> keeps working with or without these."
|
|
324
|
+
lines
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def relative_to(path, root)
|
|
328
|
+
path.start_with?("#{root}/") ? path.sub("#{root}/", "") : path
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def project_label
|
|
332
|
+
key = client.project_key
|
|
333
|
+
key ? "#{File.basename(key.app_root)} (#{key.app_env})" : "this directory"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def report(message, warm:)
|
|
337
|
+
logger.notice(message)
|
|
338
|
+
warm ? 0 : 1
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def report_status(info, report)
|
|
342
|
+
return report("a server for #{project_label} is running (pid #{info['pid']}) but not " \
|
|
343
|
+
"answering yet", warm: false) if report.nil?
|
|
344
|
+
|
|
345
|
+
logger.notice(status_lines(info, report).join("\n"))
|
|
346
|
+
0
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def status_lines(info, report)
|
|
350
|
+
lines = ["#{project_label} is warm -- siding #{report['tool_version'] || Siding::VERSION}"]
|
|
351
|
+
lines << " server pid #{info['pid']}, booted #{format_time(info['booted_at'])}" \
|
|
352
|
+
"#{format_boot_duration(report['boot_seconds'] || info['boot_seconds'])}"
|
|
353
|
+
lines << " revision #{report['revision_label'] || info['revision_label']}"
|
|
354
|
+
lines << " served #{format_served(report)}"
|
|
355
|
+
lines << " watch #{report['watch'] || 'unknown'}"
|
|
356
|
+
lines << " running #{format_workers(report['workers'])}"
|
|
357
|
+
lines << " idle exit #{format_idle_exit(report)}"
|
|
358
|
+
lines.concat(event_lines(Array(report["events"])))
|
|
359
|
+
lines
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def event_lines(events)
|
|
363
|
+
return [" recent no changes have cost anything yet"] if events.empty?
|
|
364
|
+
|
|
365
|
+
[" recent"] + events.reverse.map do |event|
|
|
366
|
+
paths = Array(event["trigger_paths"]).first(2).join(", ")
|
|
367
|
+
detail = [event["reason"], paths].reject { |part| part.to_s.empty? }.join(": ")
|
|
368
|
+
" #{format_time(event['at'])} #{event['resolution']} #{detail}"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def format_served(report)
|
|
373
|
+
served = report["served"].to_i
|
|
374
|
+
last = report["last_activity_at"]
|
|
375
|
+
"#{served} invocation#{'s' unless served == 1}" \
|
|
376
|
+
"#{last ? ", last #{format_ago(Time.now.to_f - last.to_f)} ago" : ''}"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def format_workers(workers)
|
|
380
|
+
workers = Array(workers)
|
|
381
|
+
workers.empty? ? "nothing" : "#{workers.size} worker(s): #{workers.join(', ')}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def format_idle_exit(report)
|
|
385
|
+
timeout = report["idle_timeout"].to_f
|
|
386
|
+
return "not scheduled" unless timeout.positive?
|
|
387
|
+
|
|
388
|
+
last = report["last_activity_at"].to_f
|
|
389
|
+
remaining = last.positive? ? timeout - (Time.now.to_f - last) : timeout
|
|
390
|
+
remaining.positive? ? "in #{format_ago(remaining)}" : "due now"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def format_boot_duration(seconds)
|
|
394
|
+
seconds.to_f.positive? ? " in #{format('%.1f', seconds)}s" : ""
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def format_time(value)
|
|
398
|
+
return "unknown" if value.nil?
|
|
399
|
+
return value if value.is_a?(String)
|
|
400
|
+
|
|
401
|
+
Time.at(value.to_f).strftime("%Y-%m-%d %H:%M:%S")
|
|
402
|
+
rescue StandardError
|
|
403
|
+
"unknown"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def format_ago(seconds)
|
|
407
|
+
seconds = seconds.to_f
|
|
408
|
+
return "#{seconds.round}s" if seconds < 90
|
|
409
|
+
|
|
410
|
+
minutes = seconds / 60
|
|
411
|
+
minutes < 90 ? "#{minutes.round}m" : "#{(minutes / 60).round}h"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def request_status(runtime)
|
|
415
|
+
request(runtime, Protocol::STATUS) do |socket|
|
|
416
|
+
message = Protocol.read_message(socket)
|
|
417
|
+
message && message.type == Protocol::STATUS_REPORT ? message.payload : nil
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def request_stop(runtime)
|
|
422
|
+
request(runtime, Protocol::STOP) { |socket| Protocol.read_message(socket) }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def request(runtime, type)
|
|
426
|
+
socket = UNIXSocket.new(runtime.socket_path)
|
|
427
|
+
socket.timeout = REQUEST_TIMEOUT if socket.respond_to?(:timeout=)
|
|
428
|
+
begin
|
|
429
|
+
Protocol.client_handshake(socket)
|
|
430
|
+
Protocol.write_message(socket, type)
|
|
431
|
+
yield socket
|
|
432
|
+
ensure
|
|
433
|
+
socket.close unless socket.closed?
|
|
434
|
+
end
|
|
435
|
+
rescue StandardError => e
|
|
436
|
+
logger.debug("#{type} request failed: #{e.class}: #{e.message}")
|
|
437
|
+
nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def unimplemented(name)
|
|
441
|
+
logger.notice("`siding #{name}` is not implemented yet")
|
|
442
|
+
1
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def terminate(pid, grace: 5.0)
|
|
446
|
+
Process.kill("TERM", pid)
|
|
447
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + grace
|
|
448
|
+
sleep 0.02 while alive?(pid) && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
449
|
+
return unless alive?(pid)
|
|
450
|
+
|
|
451
|
+
logger.debug("server #{pid} did not leave within #{grace}s; killing it")
|
|
452
|
+
Process.kill("KILL", pid)
|
|
453
|
+
rescue Errno::ESRCH
|
|
454
|
+
nil
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def alive?(pid) = Runtime.process_alive?(pid)
|
|
458
|
+
end
|
|
459
|
+
end
|