From 415437f12c93710be3388cfbef983c9b07c5a8de Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 13 Mar 2026 20:54:53 -0300 Subject: [PATCH 01/11] Print threads backtrace on SIGINFO/SIGPWR --- lib/cucumber/cli/main.rb | 21 +++++++++++++++++++++ spec/cucumber/cli/main_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/cucumber/cli/main.rb b/lib/cucumber/cli/main.rb index a550ac984..eca2d7ddc 100644 --- a/lib/cucumber/cli/main.rb +++ b/lib/cucumber/cli/main.rb @@ -23,6 +23,7 @@ def initialize(args, out = $stdout, err = $stderr, kernel = Kernel) def execute!(existing_runtime = nil) trap_interrupt + trap_thread_dump_signal runtime = runtime(existing_runtime) @@ -90,6 +91,26 @@ def trap_interrupt end end + def trap_thread_dump_signal + signal = Signal.list['INFO'] || Signal.list['PWR'] + + return if signal.nil? + + trap(signal) do + Thread.list.each do |thread| + prefix = "Thread TID-#{(thread.object_id ^ Process.pid).to_s(36)} #{thread.name || ''}" + + if thread.backtrace&.any? + thread.backtrace.each do |line| + @err.puts("#{prefix} #{line}") + end + else + @err.puts("#{prefix} ") + end + end + end + end + def runtime(existing_runtime) return Runtime.new(configuration) unless existing_runtime diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 8183d5c1e..2d143cb59 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -65,6 +65,26 @@ def do_execute subject.execute! end end + + thread_dump_signal = Signal.list['INFO'] || Signal.list['PWR'] + + context 'when interrupted with thread dump signal', skip: thread_dump_signal.nil? do + let(:runtime) { double('runtime').as_null_object } + + it 'dumps the thread backtrace to the error stream' do + allow(runtime).to receive(:run!) do + Process.kill(thread_dump_signal, Process.pid) + end + + allow(runtime).to receive(:failure?).and_return(false) + + expect(kernel).to receive(:exit).with(0) + + subject.execute!(runtime) + + expect(stderr.string).to match(/Thread TID-[a-z0-9]+ #{__FILE__}:#{__LINE__ - 8}:in 'Process\.kill'/) + end + end end [ProfilesNotDefinedError, YmlLoadError, ProfileNotFound].each do |exception_klass| From 4c71d3ae624cc5f1976d89dfaebbd6e32e464970 Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 13 Mar 2026 20:58:03 -0300 Subject: [PATCH 02/11] Add CHANGELOG.md entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2cbad7c..f0d3c2878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo - A first initial iteration of the new `cucumber-query` structure ([#1801](https://github.com/cucumber/cucumber-ruby/pull/1801) [luke-hill](https://github.com/luke-hill)) > This will be used for the migration of all existing formatters - becoming the building blocks for the future of cucumber formatters > which will begin being migrated in the start of 2026 +- Print threads backtrace on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho) ### Fixed - Fix crash when `Cucumber::Messages::Group#children` is `nil` From 6106cd078bc8d39c847a5a4bebd2862ed544c894 Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 13 Mar 2026 21:00:01 -0300 Subject: [PATCH 03/11] Fix spec --- spec/cucumber/cli/main_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 2d143cb59..0019feccf 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -82,7 +82,9 @@ def do_execute subject.execute!(runtime) - expect(stderr.string).to match(/Thread TID-[a-z0-9]+ #{__FILE__}:#{__LINE__ - 8}:in 'Process\.kill'/) + tid = (Thread.current.object_id ^ Process.pid).to_s(36) + + expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{__LINE__ - 11}:in 'Process\.kill'/) end end end From f9a6e930b07452a8695bf5cbbedaf5e0a928cbd9 Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Mon, 16 Mar 2026 18:45:17 -0300 Subject: [PATCH 04/11] Fix spec for older rubies --- spec/cucumber/cli/main_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 0019feccf..5c530c4f3 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -84,7 +84,7 @@ def do_execute tid = (Thread.current.object_id ^ Process.pid).to_s(36) - expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{__LINE__ - 11}:in 'Process\.kill'/) + expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{__LINE__ - 11}:in (?:`kill'|'Process\.kill')/) end end end From 2e73a6306adbb78db592a07b5ab158140e06d70e Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 10 Apr 2026 13:03:42 -0300 Subject: [PATCH 05/11] Do not call thread.backtrace twice --- lib/cucumber/cli/main.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/cucumber/cli/main.rb b/lib/cucumber/cli/main.rb index eca2d7ddc..b84674df2 100644 --- a/lib/cucumber/cli/main.rb +++ b/lib/cucumber/cli/main.rb @@ -99,9 +99,10 @@ def trap_thread_dump_signal trap(signal) do Thread.list.each do |thread| prefix = "Thread TID-#{(thread.object_id ^ Process.pid).to_s(36)} #{thread.name || ''}" + backtrace = thread.backtrace - if thread.backtrace&.any? - thread.backtrace.each do |line| + if backtrace&.any? + backtrace.each do |line| @err.puts("#{prefix} #{line}") end else From 7f32dbaa1701608beba5af8728f7269844537c40 Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 10 Apr 2026 13:05:42 -0300 Subject: [PATCH 06/11] Capture the kill line to a var --- spec/cucumber/cli/main_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 2fa4fa6d1..43b2a3987 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -69,8 +69,11 @@ def do_execute let(:runtime) { double('runtime').as_null_object } it 'dumps the thread backtrace to the error stream' do + kill_line = 0 + allow(runtime).to receive(:run!) do Process.kill(thread_dump_signal, Process.pid) + kill_line = __LINE__ - 1 end allow(runtime).to receive(:failure?).and_return(false) @@ -81,7 +84,7 @@ def do_execute tid = (Thread.current.object_id ^ Process.pid).to_s(36) - expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{__LINE__ - 11}:in (?:`kill'|'Process\.kill')/) + expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{kill_line}:in (?:`kill'|'Process\.kill')/) end end end From c4c42fe3e50e8e1e74b40946bda7bc94077d306e Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 10 Apr 2026 13:06:39 -0300 Subject: [PATCH 07/11] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d3c2878..d7ecba47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo - A first initial iteration of the new `cucumber-query` structure ([#1801](https://github.com/cucumber/cucumber-ruby/pull/1801) [luke-hill](https://github.com/luke-hill)) > This will be used for the migration of all existing formatters - becoming the building blocks for the future of cucumber formatters > which will begin being migrated in the start of 2026 -- Print threads backtrace on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho) +- Print thread backtraces on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho) ### Fixed - Fix crash when `Cucumber::Messages::Group#children` is `nil` From 36ed3eb7d03cd23677bada5c655a654a470da760 Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 10 Apr 2026 15:54:04 -0300 Subject: [PATCH 08/11] Extract pattern to make it easier to remove when Ruby 3.4 is gone --- spec/cucumber/cli/main_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 43b2a3987..0d40d1c0c 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -83,8 +83,9 @@ def do_execute subject.execute!(runtime) tid = (Thread.current.object_id ^ Process.pid).to_s(36) + pattern = RUBY_VERSION >= '3.4' ? /'Process\.kill'/ : /`kill'/ - expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{kill_line}:in (?:`kill'|'Process\.kill')/) + expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{kill_line}:in #{pattern}/) end end end From 9074da2e3f682283899f6347567e3f70ffc2c7fe Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Fri, 17 Apr 2026 14:59:15 -0300 Subject: [PATCH 09/11] Fix spec for TruffleRuby --- spec/cucumber/cli/main_spec.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 99bc20389..5533f57fd 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -87,9 +87,14 @@ def do_execute subject.execute!(runtime) tid = (Thread.current.object_id ^ Process.pid).to_s(36) - pattern = RUBY_VERSION >= '3.4' ? /'Process\.kill'/ : /`kill'/ - expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{kill_line}:in #{pattern}/) + if defined?(TruffleRuby) + # TruffleRuby does not dump the actual Process.kill call, but rather the internal core/thread.rb call + expect(stderr.string).to match(/Thread TID-#{tid} core\/thread.rb:/) + else + pattern = RUBY_VERSION >= '3.4' ? /'Process\.kill'/ : /`kill'/ + expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{kill_line}:in #{pattern}/) + end end end end From 16381255200f9878215652af1e2a8e61bd055eab Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Tue, 28 Apr 2026 17:18:12 -0300 Subject: [PATCH 10/11] Fix SIGPWR trap on JRuby On JRuby, trap(integer) silently fails to register the handler, causing the process to terminate with exit code 158 when SIGPWR is received. Using the signal name as a string (trap('PWR')) works correctly. Additionally, JRuby executes signal handlers on a dedicated thread asynchronously, so the spec waits for the handler thread to appear in the output rather than checking the main thread's backtrace. Co-Authored-By: Claude Sonnet 4.6 --- lib/cucumber/cli/main.rb | 2 +- spec/cucumber/cli/main_spec.rb | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/cucumber/cli/main.rb b/lib/cucumber/cli/main.rb index b84674df2..2b14732a2 100644 --- a/lib/cucumber/cli/main.rb +++ b/lib/cucumber/cli/main.rb @@ -92,7 +92,7 @@ def trap_interrupt end def trap_thread_dump_signal - signal = Signal.list['INFO'] || Signal.list['PWR'] + signal = %w[INFO PWR].find { |s| Signal.list.key?(s) } return if signal.nil? diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 5533f57fd..9ccb85b4d 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -67,7 +67,7 @@ def do_execute end end - thread_dump_signal = Signal.list['INFO'] || Signal.list['PWR'] + thread_dump_signal = %w[INFO PWR].find { |s| Signal.list.key?(s) } context 'when interrupted with thread dump signal', skip: thread_dump_signal.nil? do let(:runtime) { double('runtime').as_null_object } @@ -88,7 +88,15 @@ def do_execute tid = (Thread.current.object_id ^ Process.pid).to_s(36) - if defined?(TruffleRuby) + if defined?(JRUBY_VERSION) + pattern = /Thread TID-[0-9a-z]+ SIGPWR handler / + + # JRuby runs signal handlers asynchronously on a dedicated thread + deadline = Time.now + 2 + sleep 0.01 while stderr.string !~ pattern && Time.now < deadline + + expect(stderr.string).to match(pattern) + elsif defined?(TruffleRuby) # TruffleRuby does not dump the actual Process.kill call, but rather the internal core/thread.rb call expect(stderr.string).to match(/Thread TID-#{tid} core\/thread.rb:/) else From d7b59fb145736e70d9a8a91eb90391cab18b1a8c Mon Sep 17 00:00:00 2001 From: Gabriel Sobrinho Date: Thu, 30 Apr 2026 07:11:24 -0300 Subject: [PATCH 11/11] Make rubocop happy --- spec/cucumber/cli/main_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index 9ccb85b4d..cce413508 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -94,15 +94,15 @@ def do_execute # JRuby runs signal handlers asynchronously on a dedicated thread deadline = Time.now + 2 sleep 0.01 while stderr.string !~ pattern && Time.now < deadline - - expect(stderr.string).to match(pattern) elsif defined?(TruffleRuby) # TruffleRuby does not dump the actual Process.kill call, but rather the internal core/thread.rb call - expect(stderr.string).to match(/Thread TID-#{tid} core\/thread.rb:/) + pattern = /Thread TID-#{tid} core\/thread.rb:/ else pattern = RUBY_VERSION >= '3.4' ? /'Process\.kill'/ : /`kill'/ - expect(stderr.string).to match(/Thread TID-#{tid} #{__FILE__}:#{kill_line}:in #{pattern}/) + pattern = /Thread TID-#{tid} #{__FILE__}:#{kill_line}:in #{pattern}/ end + + expect(stderr.string).to match(pattern) end end end