Skip to content

Commit

Permalink
Add Process::Status#description (#15468)
Browse files Browse the repository at this point in the history
In the compiler we have a neat feature that shows a human readable description when a process process exited with an abnormal status.
This patch moves this as a gernal tool into stdlib as `Process::Status#description`.

The implementation is split into two parts:
* `Process::ExitReason#description` provides the static description for most exit reasons.
* `Process::Status#description` enhances that description with details about the termination signal when it's a signal exit without further specification.

Co-authored-by: Sijawusz Pur Rahnama <[email protected]>
  • Loading branch information
straight-shoota and Sija authored Feb 28, 2025
1 parent b847480 commit fbb005d
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 30 deletions.
10 changes: 10 additions & 0 deletions spec/std/process/exit_reason_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require "spec"

describe Process::ExitReason do
describe "#description" do
it "with exit status" do
Process::ExitReason::Normal.description.should eq "Process exited normally"
Process::ExitReason::Unknown.description.should eq "Process terminated abnormally, the cause is unknown"
end
end
end
24 changes: 24 additions & 0 deletions spec/std/process/status_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,28 @@ describe Process::Status do
end
{% end %}
end

describe "#description" do
it "with exit status" do
Process::Status.new(exit_status(0)).description.should eq "Process exited normally"
Process::Status.new(exit_status(255)).description.should eq "Process exited normally"
end

it "on interrupt" do
status_for(:interrupted).description.should eq "Process was interrupted"
end

{% if flag?(:unix) && !flag?(:wasi) %}
it "with exit signal" do
Process::Status.new(Signal::HUP.value).description.should eq "Process terminated abnormally"
Process::Status.new(Signal::KILL.value).description.should eq "Process terminated abnormally"
Process::Status.new(Signal::STOP.value).description.should eq "Process received and didn't handle signal STOP"
last_signal = Signal.values[-1]
Process::Status.new(last_signal.value).description.should eq "Process received and didn't handle signal #{last_signal}"

unknown_signal = Signal.new(126)
Process::Status.new(unknown_signal.value).description.should eq "Process received and didn't handle signal 126"
end
{% end %}
end
end
32 changes: 2 additions & 30 deletions src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -305,42 +305,14 @@ class Crystal::Command
exit exit_code
end

if message = exit_message(status)
STDERR.puts message
unless status.exit_reason.normal?
STDERR.puts status.description
STDERR.flush
end

exit 1
end

private def exit_message(status)
case status.exit_reason
when .aborted?, .session_ended?, .terminal_disconnected?
if signal = status.exit_signal?
if signal.kill?
"Program was killed"
else
"Program received and didn't handle signal #{signal} (#{signal.value})"
end
else
"Program exited abnormally"
end
when .breakpoint?
"Program hit a breakpoint and no debugger was attached"
when .access_violation?, .bad_memory_access?
# NOTE: this only happens with the empty prelude, because the stdlib
# runtime catches those exceptions and then exits _normally_ with exit
# code 11 or 1
"Program exited because of an invalid memory access"
when .bad_instruction?
"Program exited because of an invalid instruction"
when .float_exception?
"Program exited because of a floating-point system exception"
when .unknown?
"Program exited abnormally, the cause is unknown"
end
end

record CompilerConfig,
compiler : Compiler,
sources : Array(Compiler::Source),
Expand Down
49 changes: 49 additions & 0 deletions src/process/status.cr
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ enum Process::ExitReason
def abnormal?
!normal?
end

# Returns a textual description of this exit reason.
#
# ```
# Process::ExitReason::Normal.description # => "Process exited normally"
# Process::ExitReason::Aborted.description # => "Process terminated abnormally"
# ```
#
# `Status#description` provides more detail for a specific process status.
def description
case self
in .normal?
"Process exited normally"
in .aborted?, .session_ended?, .terminal_disconnected?
"Process terminated abnormally"
in .interrupted?
"Process was interrupted"
in .breakpoint?
"Process hit a breakpoint and no debugger was attached"
in .access_violation?, .bad_memory_access?
"Process terminated because of an invalid memory access"
in .bad_instruction?
"Process terminated because of an invalid instruction"
in .float_exception?
"Process terminated because of a floating-point system exception"
in .signal?
"Process terminated because of an unhandled signal"
in .unknown?
"Process terminated abnormally, the cause is unknown"
end
end
end

# The status of a terminated process. Returned by `Process#wait`.
Expand Down Expand Up @@ -371,6 +402,24 @@ class Process::Status
{% end %}
end

# Returns a textual description of this process status.
#
# ```
# Process::Status.new(0).description # => "Process exited normally"
# process = Process.new("sleep", ["10"])
# process.terminate
# process.wait.description # => "Process received and didn't handle signal TERM (15)"
# ```
#
# `ExitReason#description` provides the specific messages for non-signal exits.
def description
if exit_reason.signal? && (signal = exit_signal?)
"Process received and didn't handle signal #{signal}"
else
exit_reason.description
end
end

private def stringify_exit_status_windows(io)
# On Windows large status codes are typically expressed in hexadecimal
if @exit_status >= UInt16::MAX
Expand Down

0 comments on commit fbb005d

Please sign in to comment.