Skip to content

Commit 8257cbe

Browse files
committed
fixed puma integration
1 parent 382e363 commit 8257cbe

4 files changed

Lines changed: 172 additions & 22 deletions

File tree

.cspell/project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ xorshift
6868
yardoc
6969
hostnames
7070
ESRCH
71+
demodulize

lib/log_struct/integrations/puma.rb

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Puma
1212
installed: false,
1313
boot_emitted: false,
1414
shutdown_emitted: false,
15+
handler_pending_started: false,
1516
start_info: {
1617
mode: nil,
1718
puma_version: nil,
@@ -158,6 +159,7 @@ def state_reset!
158159
STATE[:boot_emitted] = false
159160
STATE[:shutdown_emitted] = false
160161
STATE[:started_emitted] = false
162+
STATE[:handler_pending_started] = false
161163
STATE[:start_info] = {
162164
mode: nil,
163165
puma_version: nil,
@@ -223,7 +225,9 @@ def process_line(line)
223225

224226
if (m = l.match(/^\*?\s*Listening on (.+)$/))
225227
si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
226-
si[:listening] << T.must(m[1])
228+
list = T.cast(si[:listening], T::Array[T.untyped])
229+
address = T.must(m[1])
230+
list << address unless list.include?(address)
227231
# Emit started when we see the first listening address
228232
if !STATE[:started_emitted]
229233
emit_started!
@@ -258,7 +262,7 @@ def process_line(line)
258262
emit_started!
259263
STATE[:started_emitted] = true
260264
end
261-
return true
265+
return false
262266
end
263267

264268
if l.start_with?("- Gracefully stopping")
@@ -309,6 +313,7 @@ def emit_started!
309313
timestamp: Time.now
310314
)
311315
LogStruct.info(log)
316+
STATE[:handler_pending_started] = false
312317
# Only use LogStruct; SemanticLogger routes to STDOUT in test
313318
end
314319

@@ -393,43 +398,75 @@ def log(str)
393398
module RackHandlerPatch
394399
extend T::Sig
395400

396-
sig { params(app: T.untyped, options: T.untyped).returns(T.untyped) }
397-
def run(app, options)
398-
# Emit started once per process
401+
sig do
402+
params(
403+
app: T.untyped,
404+
args: T.untyped,
405+
block: T.nilable(T.proc.returns(T.untyped))
406+
).returns(T.untyped)
407+
end
408+
def run(app, *args, &block)
409+
rest = args
410+
options = T.let({}, T::Hash[T.untyped, T.untyped])
411+
rest.each do |value|
412+
next unless value.is_a?(Hash)
413+
options.merge!(value)
414+
end
415+
399416
begin
417+
si = T.cast(::LogStruct::Integrations::Puma::STATE[:start_info], T::Hash[Symbol, T.untyped])
418+
si[:mode] ||= "single"
419+
si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
420+
si[:pid] ||= Process.pid
421+
si[:listening] ||= []
400422
port = T.let(nil, T.untyped)
401423
host = T.let(nil, T.untyped)
402424
if options.respond_to?(:[])
403425
port = options[:Port] || options["Port"] || options[:port] || options["port"]
404426
host = options[:Host] || options["Host"] || options[:host] || options["host"]
405427
end
406-
addr = if port
428+
if port
429+
list = T.cast(si[:listening], T::Array[T.untyped])
430+
list.clear
407431
h = (host && host != "0.0.0.0") ? host : "127.0.0.1"
408-
["tcp://#{h}:#{port}"]
432+
list << "tcp://#{h}:#{port}"
409433
end
410-
started = ::LogStruct::Log::Puma::Started.new(
411-
mode: "single",
412-
environment: (defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil,
413-
process_id: Process.pid,
414-
listening_addresses: addr
415-
)
416-
::LogStruct.info(started)
434+
state = ::LogStruct::Integrations::Puma::STATE
435+
state[:handler_pending_started] = true unless state[:started_emitted]
417436
rescue => e
418437
::LogStruct::Integrations::Puma.handle_integration_error(e)
419438
end
420439

421440
begin
422441
Kernel.at_exit do
423-
shutdown = ::LogStruct::Log::Puma::Shutdown.new(process_id: Process.pid)
424-
::LogStruct.info(shutdown)
442+
unless ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted]
443+
::LogStruct::Integrations::Puma.emit_shutdown!("Exiting")
444+
::LogStruct::Integrations::Puma::STATE[:shutdown_emitted] = true
445+
end
425446
rescue => e
426447
::LogStruct::Integrations::Puma.handle_integration_error(e)
427448
end
428449
rescue => e
429450
::LogStruct::Integrations::Puma.handle_integration_error(e)
430451
end
431452

432-
super
453+
begin
454+
result = super(app, **options, &block)
455+
ensure
456+
state = ::LogStruct::Integrations::Puma::STATE
457+
if state[:handler_pending_started] && !state[:started_emitted]
458+
begin
459+
::LogStruct::Integrations::Puma.emit_started!
460+
state[:started_emitted] = true
461+
rescue => e
462+
::LogStruct::Integrations::Puma.handle_integration_error(e)
463+
ensure
464+
state[:handler_pending_started] = false
465+
end
466+
end
467+
end
468+
469+
result
433470
end
434471
end
435472

lib/log_struct/rails_boot_banner_silencer.rb

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,103 @@ def self.install!
2020

2121
sig { void }
2222
def self.patch!
23-
# Do not suppress Rails::Server boot info completely; we rely on
24-
# some of those lines for Puma readiness parsing.
23+
begin
24+
require "rails/command"
25+
require "rails/commands/server/server_command"
26+
rescue LoadError
27+
# Best-effort – if Rails isn't available yet we'll try again later
28+
return
29+
end
2530

26-
# Also silence Thor/ServerCommand banner printing used by Rails CLI
27-
# Minimal silencer: leave as no-op to avoid Sorbet issues.
28-
nil
31+
server_command = T.let(nil, T.untyped)
32+
# rubocop:disable Sorbet/ConstantsFromStrings
33+
begin
34+
server_command = ::Object.const_get("Rails::Command::ServerCommand")
35+
rescue NameError
36+
server_command = nil
37+
end
38+
# rubocop:enable Sorbet/ConstantsFromStrings
39+
return unless server_command
40+
41+
patch_server_command(server_command)
42+
end
43+
44+
sig { params(server_command: T.untyped).void }
45+
def self.patch_server_command(server_command)
46+
return if server_command <= ServerCommandSilencer
47+
48+
server_command.prepend(ServerCommandSilencer)
49+
end
50+
51+
module ServerCommandSilencer
52+
extend T::Sig
53+
54+
sig { params(args: T.untyped, block: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
55+
def perform(*args, &block)
56+
mark_server_mode!
57+
super
58+
end
59+
60+
sig { params(server: T.untyped, url: T.nilable(String)).void }
61+
def print_boot_information(server, url)
62+
mark_server_mode!
63+
consume_boot_banner(server, url)
64+
end
65+
66+
private
67+
68+
sig { void }
69+
def mark_server_mode!
70+
::LogStruct.instance_variable_set(:@server_mode, true)
71+
rescue
72+
# Ignore – server mode is best-effort
73+
end
74+
75+
sig { params(server: T.untyped, url: T.nilable(String)).void }
76+
def consume_boot_banner(server, url)
77+
return unless defined?(::LogStruct::Integrations::Puma)
78+
79+
begin
80+
::LogStruct::Integrations::Puma.emit_boot_if_needed!
81+
rescue => e
82+
::LogStruct::Integrations::Puma.handle_integration_error(e)
83+
end
84+
85+
begin
86+
model = ::ActiveSupport::Inflector.demodulize(server)
87+
rescue
88+
model = "Puma"
89+
end
90+
91+
lines = [
92+
"=> Booting #{model}",
93+
build_rails_banner_line(url),
94+
"=> Run `#{lookup_executable} --help` for more startup options"
95+
]
96+
97+
lines.each do |line|
98+
::LogStruct::Integrations::Puma.process_line(line)
99+
rescue => e
100+
::LogStruct::Integrations::Puma.handle_integration_error(e)
101+
end
102+
end
103+
104+
sig { params(url: T.nilable(String)).returns(String) }
105+
def build_rails_banner_line(url)
106+
suffix = url ? " #{url}" : ""
107+
"=> Rails #{::Rails.version} application starting in #{::Rails.env}#{suffix}"
108+
rescue
109+
"=> Rails application starting"
110+
end
111+
112+
sig { returns(String) }
113+
def lookup_executable
114+
return "rails" unless T.unsafe(self).respond_to?(:executable, true)
115+
116+
T.cast(T.unsafe(self).send(:executable), String)
117+
rescue
118+
"rails"
119+
end
29120
end
30121
end
31122
end

rails_test_app/create_app.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,27 @@ def copy_template(file, target_path = nil)
192192
gemfile_content.sub!(/^(\s*gem\s+"rails".*$)/, "\\1\n# Use sqlite3 as the database for Active Record\n gem \"sqlite3\", \">= 2.1\"")
193193
end
194194

195+
# Pin Puma version to exercise both legacy and modern releases across the matrix
196+
puma_requirement = if @rails_major_minor == "7.0"
197+
"~> 6.4"
198+
else
199+
"~> 7.0"
200+
end
201+
202+
if gemfile_content.match?(/^(\s*# Use the Puma web server.*\n)(\s*gem\s+"puma".*$)/)
203+
gemfile_content.gsub!(
204+
/(\s*# Use the Puma web server.*\n)\s*gem\s+"puma".*$/,
205+
"\\1 gem \"puma\", \"#{puma_requirement}\""
206+
)
207+
elsif gemfile_content.match?(/^\s*gem\s+"puma"/)
208+
gemfile_content.gsub!(/^\s*gem\s+"puma".*$/, "gem \"puma\", \"#{puma_requirement}\"")
209+
else
210+
gemfile_content.sub!(
211+
/^(\s*gem\s+"rails".*$)/,
212+
"\\1\n# Use the Puma web server\n gem \"puma\", \"#{puma_requirement}\""
213+
)
214+
end
215+
195216
# Add LogStruct gem
196217
logstruct_gem_line = "# LogStruct gem from local path\ngem \"logstruct\", path: \"#{ROOT_DIR}\"\n\n"
197218
if gemfile_content.include?("logstruct")

0 commit comments

Comments
 (0)