From 22f33dc455fd6f4f69c41ebfc9fee034effd85cb 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/3] 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 84aa62c9..b64b6b89 100644 --- a/lib/spring/application.rb +++ b/lib/spring/application.rb @@ -378,10 +378,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 47137a0a64dc6b2acce8589a5c9b88addb80e410 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/3] 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 ee08721fe626498a7053ec7a974578108a4dafdd 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/3] Test signal exit code scenario. --- test/support/acceptance_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/support/acceptance_test.rb b/test/support/acceptance_test.rb index d4d7f6ed..6043e4f4 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -747,6 +747,12 @@ 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}" + end end end end