From b8818c45757a8a3e26fbaf3ff6f2a2b8a557641a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chary=C5=82o?= Date: Fri, 20 Jun 2025 15:14:43 +0200 Subject: [PATCH 1/4] Pass signaled exit code properly to the client Process::Status#existstatus is nil when child did not exit cleanly. When ruby process crashes, running it with spring masked exit code and returned 0. This commit allows Spring::Server thread to properly pass application exit code to child, even when signaled or stopped. Fixes #676. --- lib/spring/application.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/spring/application.rb b/lib/spring/application.rb index f21e4f64..da5cb9d2 100644 --- a/lib/spring/application.rb +++ b/lib/spring/application.rb @@ -377,10 +377,10 @@ def wait(pid, streams, client) Spring.failsafe_thread { begin _, status = Process.wait2 pid - log "#{pid} exited with #{status.exitstatus}" + log "#{pid} exited with #{status.exitstatus || status.inspect}" streams.each(&:close) - client.puts(status.exitstatus) + client.puts(status.exitstatus || status.to_i) client.close ensure @mutex.synchronize { @waiting.delete pid } From 6bedfad5cc577713476da3ac479d369822af0dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chary=C5=82o?= Date: Fri, 20 Jun 2025 15:18:06 +0200 Subject: [PATCH 2/4] Expect exit status code in spring client In the previous commit I fixed a scenario where Spring Server failed to pass the application exit code through to Spring Client. Should similar thing happen in future, this can also be detected in Spring Client. It should expect to read some integer and not default to 0 when read nil. This commit introduces such assertion in Spring Client. Also fixes #676. @see 3a8e6096eb129e9b3301dc27986f233e9df5a82f --- lib/spring/client/run.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/spring/client/run.rb b/lib/spring/client/run.rb index 51ce51e8..52ec20cf 100644 --- a/lib/spring/client/run.rb +++ b/lib/spring/client/run.rb @@ -184,11 +184,16 @@ def run_command(client, application) suspend_resume_on_tstp_cont(pid) forward_signals(application) - status = application.read.to_i + status = application.read + log "got exit status #{status.inspect}" - log "got exit status #{status}" + # Status should always be an integer. If it is empty, something unexpected must have happened to the server. + if status.to_s.strip.empty? + log "unexpected empty exit status, app crashed?" + exit 1 + end - exit status + exit status.to_i else log "got no pid" exit 1 From 43ed7f60ca1dde857011c092937438d8f560d331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chary=C5=82o?= Date: Fri, 20 Jun 2025 15:56:50 +0200 Subject: [PATCH 3/4] Test signal exit code scenario. --- test/support/acceptance_test.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/support/acceptance_test.rb b/test/support/acceptance_test.rb index 34876755..dde05801 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -745,6 +745,16 @@ class MyEngine < Rails::Engine assert_failure app.spring_test_command, stderr: "omg (RuntimeError)" end + + test "passes exit code from exit and signal" do + artifacts = app.run("bin/rails runner 'Process.exit(7)'") + code = artifacts[:status].exitstatus || artifacts[:status].termsig + assert_equal 7, code, "Expected exit status to be 7, but was #{code}" + + artifacts = app.run("bin/rails runner 'system(\"kill -7 \#{Process.pid}\")'") + code = artifacts[:status].exitstatus || artifacts[:status].termsig + assert_equal 7, code, "Expected exit status to be 7, but was #{code}" + end end end end From 792a62e806261adb9177d730f31276e2a6f81acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chary=C5=82o?= Date: Fri, 20 Jun 2025 16:28:15 +0200 Subject: [PATCH 4/4] Fix exit code test for unix platform UNIX adds 128 to signal codes. --- test/support/acceptance_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/acceptance_test.rb b/test/support/acceptance_test.rb index dde05801..85ac6ff6 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -753,7 +753,7 @@ class MyEngine < Rails::Engine artifacts = app.run("bin/rails runner 'system(\"kill -7 \#{Process.pid}\")'") code = artifacts[:status].exitstatus || artifacts[:status].termsig - assert_equal 7, code, "Expected exit status to be 7, but was #{code}" + assert_equal 7, code % 128, "Expected exit status to be 7, but was #{code}" end end end