From 3a8e6096eb129e9b3301dc27986f233e9df5a82f 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 d713e82e..de0467c1 100644 --- a/lib/spring/application.rb +++ b/lib/spring/application.rb @@ -370,10 +370,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 2fac8e435ada989cb30ab09a77055ef6b1339894 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 05b1675056fc4af67841551551461266f777797d 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 79f9b71a..f240ca4f 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -740,6 +740,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 2dc1b547258014c75b72eaebe77211adc7ef9d2b 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 f240ca4f..5d4b1237 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -748,7 +748,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