Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Process::ExitReason and Process::Status#exit_reason #13052

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 103 additions & 3 deletions spec/std/process/status_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,116 @@ describe Process::Status do
end

it "#normal_exit? with signal code" do
Process::Status.new(0x00).normal_exit?.should be_true
Process::Status.new(0x01).normal_exit?.should be_false
Process::Status.new(0x7e).normal_exit?.should be_false
Process::Status.new(0x7f).normal_exit?.should be_false
end

it "#signal_exit? with signal code" do
Process::Status.new(0x00).signal_exit?.should be_false
Process::Status.new(0x01).signal_exit?.should be_true

# 0x7f raises arithmetic error due to overflow, but this shouldn't
# matter because actual signal values don't expand to that range
Process::Status.new(0x7e).signal_exit?.should be_true
Process::Status.new(0x7f).signal_exit?.should be_false
end
{% end %}

{% if flag?(:win32) %}
describe "#exit_reason" do
it "returns Normal" do
Process::Status.new(exit_status(0)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(1)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(127)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(128)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(255)).exit_reason.normal?.should be_true

Process::Status.new(0x3FFFFFFF_u32).exit_reason.normal?.should be_true
Process::Status.new(0x40001234_u32).exit_reason.normal?.should be_false
Process::Status.new(0x80001234_u32).exit_reason.normal?.should be_false
Process::Status.new(0xC0001234_u32).exit_reason.normal?.should be_false
end

it "returns Aborted" do
Process::Status.new(0x40000015_u32).exit_reason.aborted?.should be_true
end

it "returns Interrupted" do
Process::Status.new(0xC000013A_u32).exit_reason.interrupted?.should be_true
end

it "returns Breakpoint" do
Process::Status.new(0x80000003_u32).exit_reason.breakpoint?.should be_true
end

it "returns AccessViolation" do
Process::Status.new(0xC0000005_u32).exit_reason.access_violation?.should be_true
Process::Status.new(0xC00000FD_u32).exit_reason.access_violation?.should be_true
end

it "returns BadMemoryAccess" do
Process::Status.new(0x80000002_u32).exit_reason.bad_memory_access?.should be_true
end

it "returns BadInstruction" do
Process::Status.new(0xC000001D_u32).exit_reason.bad_instruction?.should be_true
Process::Status.new(0xC0000096_u32).exit_reason.bad_instruction?.should be_true
end

it "returns FloatException" do
Process::Status.new(0xC000008E_u32).exit_reason.float_exception?.should be_true
Process::Status.new(0xC000008F_u32).exit_reason.float_exception?.should be_true
Process::Status.new(0xC0000090_u32).exit_reason.float_exception?.should be_true
Process::Status.new(0xC0000091_u32).exit_reason.float_exception?.should be_true
Process::Status.new(0xC0000093_u32).exit_reason.float_exception?.should be_true
end
end
{% elsif flag?(:unix) && !flag?(:wasi) %}
describe "#exit_reason" do
it "returns Normal" do
Process::Status.new(exit_status(0)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(1)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(127)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(128)).exit_reason.normal?.should be_true
Process::Status.new(exit_status(255)).exit_reason.normal?.should be_true

Process::Status.new(0x01).exit_reason.normal?.should be_false
Process::Status.new(0x7e).exit_reason.normal?.should be_false

Process::Status.new(0x017f).exit_reason.normal?.should be_false
Process::Status.new(0xffff).exit_reason.normal?.should be_false
end

it "returns Aborted" do
Process::Status.new(Signal::ABRT.value).exit_reason.aborted?.should be_true
Process::Status.new(Signal::HUP.value).exit_reason.aborted?.should be_true
Process::Status.new(Signal::KILL.value).exit_reason.aborted?.should be_true
Process::Status.new(Signal::QUIT.value).exit_reason.aborted?.should be_true
Process::Status.new(Signal::TERM.value).exit_reason.aborted?.should be_true
end

it "returns Interrupted" do
Process::Status.new(Signal::INT.value).exit_reason.interrupted?.should be_true
end

it "returns Breakpoint" do
Process::Status.new(Signal::TRAP.value).exit_reason.breakpoint?.should be_true
end

it "returns AccessViolation" do
Process::Status.new(Signal::SEGV.value).exit_reason.access_violation?.should be_true
end

it "returns BadMemoryAccess" do
Process::Status.new(Signal::BUS.value).exit_reason.bad_memory_access?.should be_true
end

it "returns BadInstruction" do
Process::Status.new(Signal::ILL.value).exit_reason.bad_instruction?.should be_true
end

it "returns FloatException" do
Process::Status.new(Signal::FPE.value).exit_reason.float_exception?.should be_true
end
end
{% end %}
end
51 changes: 36 additions & 15 deletions src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -282,24 +282,45 @@ class Crystal::Command
puts "Execute: #{elapsed_time}"
end

case status
when .normal_exit?
exit error_on_exit ? 1 : status.exit_code
when .signal_exit?
case signal = status.exit_signal
when .kill?
STDERR.puts "Program was killed"
when .segv?
STDERR.puts "Program exited because of a segmentation fault (11)"
when .int?
# OK, bubbled from the sub-program
if status.exit_reason.normal? && !error_on_exit
exit status.exit_code
end

if message = exit_message(status)
STDERR.puts message
STDERR.flush
end

exit 1
end

private def exit_message(status)
case status.exit_reason
when .aborted?
if status.signal_exit?
signal = status.exit_signal
if signal.kill?
"Program was killed"
else
"Program received and didn't handle signal #{signal} (#{signal.value})"
end
else
STDERR.puts "Program received and didn't handle signal #{signal} (#{signal.value})"
"Program exited abnormally"
end
else
STDERR.puts "Program exited abnormally, the cause is unknown"
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"
Comment on lines +297 to +322
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this couldn't be useful on its own and moved into Process::Status#description or similar?
This doesn't need to happen in this PR.

end
exit 1
end

record CompilerConfig,
Expand Down
5 changes: 3 additions & 2 deletions src/lib_c/x86_64-windows-msvc/c/errhandlingapi.cr
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
require "c/int_safe"
require "c/ntstatus"

lib LibC
EXCEPTION_CONTINUE_SEARCH = LONG.new!(0)

EXCEPTION_ACCESS_VIOLATION = 0xC0000005_u32
EXCEPTION_STACK_OVERFLOW = 0xC00000FD_u32
EXCEPTION_ACCESS_VIOLATION = LibC::STATUS_ACCESS_VIOLATION
EXCEPTION_STACK_OVERFLOW = LibC::STATUS_STACK_OVERFLOW

alias PVECTORED_EXCEPTION_HANDLER = EXCEPTION_POINTERS* -> LONG

Expand Down
17 changes: 17 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/ntstatus.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "lib_c"

lib LibC
STATUS_FATAL_APP_EXIT = 0x40000015_u32
STATUS_DATATYPE_MISALIGNMENT = 0x80000002_u32
STATUS_BREAKPOINT = 0x80000003_u32
STATUS_ACCESS_VIOLATION = 0xC0000005_u32
STATUS_ILLEGAL_INSTRUCTION = 0xC000001D_u32
STATUS_FLOAT_DIVIDE_BY_ZERO = 0xC000008E_u32
STATUS_FLOAT_INEXACT_RESULT = 0xC000008F_u32
STATUS_FLOAT_INVALID_OPERATION = 0xC0000090_u32
STATUS_FLOAT_OVERFLOW = 0xC0000091_u32
STATUS_FLOAT_UNDERFLOW = 0xC0000093_u32
STATUS_PRIVILEGED_INSTRUCTION = 0xC0000096_u32
STATUS_STACK_OVERFLOW = 0xC00000FD_u32
STATUS_CONTROL_C_EXIT = 0xC000013A_u32
end
151 changes: 141 additions & 10 deletions src/process/status.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,86 @@
{% if flag?(:win32) %}
require "c/ntstatus"
{% end %}

# The reason a process terminates.
#
# This enum provides a platform-independent way to query any exceptions that
# occurred upon a process's termination, via `Process::Status#exit_reason`.
enum Process::ExitReason
# The process exited normally.
#
# * On Unix-like systems, this implies `Process::Status#normal_exit?` is true.
# * On Windows, only exit statuses less than `0x40000000` are assumed to be
# reserved for normal exits.
Normal

# The process terminated abnormally.
#
# * On Unix-like systems, this corresponds to `Signal::ABRT`, `Signal::HUP`,
# `Signal::KILL`, `Signal::QUIT`, and `Signal::TERM`.
# * On Windows, this corresponds to the `NTSTATUS` value
# `STATUS_FATAL_APP_EXIT`.
Aborted

# The process exited due to an interrupt request.
#
# * On Unix-like systems, this corresponds to `Signal::INT`.
# * On Windows, this corresponds to the <kbd>Ctrl</kbd> + <kbd>C</kbd> and
# <kbd>Ctrl</kbd> + <kbd>Break</kbd> signals for console applications.
Interrupted

# The process reached a debugger breakpoint, but no debugger was attached.
#
# * On Unix-like systems, this corresponds to `Signal::TRAP`.
# * On Windows, this corresponds to the `NTSTATUS` value
# `STATUS_BREAKPOINT`.
Breakpoint

# The process tried to access a memory address where a read or write was not
# allowed.
#
# * On Unix-like systems, this corresponds to `Signal::SEGV`.
# * On Windows, this corresponds to the `NTSTATUS` values
# `STATUS_ACCESS_VIOLATION` and `STATUS_STACK_OVERFLOW`.
AccessViolation

# The process tried to access an invalid memory address.
#
# * On Unix-like systems, this corresponds to `Signal::BUS`.
# * On Windows, this corresponds to the `NTSTATUS` value
# `STATUS_DATATYPE_MISALIGNMENT`.
BadMemoryAccess

# The process tried to execute an invalid instruction.
#
# * On Unix-like systems, this corresponds to `Signal::ILL`.
# * On Windows, this corresponds to the `NTSTATUS` values
# `STATUS_ILLEGAL_INSTRUCTION` and `STATUS_PRIVILEGED_INSTRUCTION`.
BadInstruction

# A hardware floating-point exception occurred.
#
# * On Unix-like systems, this corresponds to `Signal::FPE`.
# * On Windows, this corresponds to the `NTSTATUS` values
# `STATUS_FLOAT_DIVIDE_BY_ZERO`, `STATUS_FLOAT_INEXACT_RESULT`,
# `STATUS_FLOAT_INVALID_OPERATION`, `STATUS_FLOAT_OVERFLOW`, and
# `STATUS_FLOAT_UNDERFLOW`.
FloatException

# The process exited due to a POSIX signal.
#
# Only applies to signals without a more specific exit reason. Unused on
# Windows.
Signal

# The process exited in a way that cannot be represented by any other
# `ExitReason`s.
#
# A `Process::Status` that maps to `Unknown` may map to a different value if
# new enum members are added to `ExitReason`.
Unknown
end

# The status of a terminated process. Returned by `Process#wait`.
class Process::Status
# Platform-specific exit status code, which usually contains either the exit code or a termination signal.
Expand All @@ -16,11 +99,66 @@ class Process::Status
end
{% end %}

# Returns a platform-independent reason that the process terminated.
def exit_reason : ExitReason
{% if flag?(:win32) %}
# TODO: perhaps this should cover everything that SEH can handle?
# https://learn.microsoft.com/en-us/windows/win32/debug/getexceptioncode
case @exit_status
when LibC::STATUS_FATAL_APP_EXIT
ExitReason::Aborted
when LibC::STATUS_CONTROL_C_EXIT
ExitReason::Interrupted
when LibC::STATUS_BREAKPOINT
ExitReason::Breakpoint
when LibC::STATUS_ACCESS_VIOLATION, LibC::STATUS_STACK_OVERFLOW
ExitReason::AccessViolation
when LibC::STATUS_DATATYPE_MISALIGNMENT
ExitReason::BadMemoryAccess
when LibC::STATUS_ILLEGAL_INSTRUCTION, LibC::STATUS_PRIVILEGED_INSTRUCTION
ExitReason::BadInstruction
when LibC::STATUS_FLOAT_DIVIDE_BY_ZERO, LibC::STATUS_FLOAT_INEXACT_RESULT, LibC::STATUS_FLOAT_INVALID_OPERATION, LibC::STATUS_FLOAT_OVERFLOW, LibC::STATUS_FLOAT_UNDERFLOW
ExitReason::FloatException
else
@exit_status & 0xC0000000_u32 == 0 ? ExitReason::Normal : ExitReason::Unknown
end
{% elsif flag?(:unix) && !flag?(:wasm32) %}
if normal_exit?
ExitReason::Normal
elsif signal_exit?
case Signal.from_value?(signal_code)
when Nil
ExitReason::Signal
when .abrt?, .hup?, .kill?, .quit?, .term?
ExitReason::Aborted
when .int?
ExitReason::Interrupted
when .trap?
ExitReason::Breakpoint
when .segv?
ExitReason::AccessViolation
when .bus?
ExitReason::BadMemoryAccess
when .ill?
ExitReason::BadInstruction
when .fpe?
ExitReason::FloatException
else
ExitReason::Signal
end
else
# TODO: stop / continue
ExitReason::Unknown
end
{% else %}
ExitReason::Normal
{% end %}
end

# Returns `true` if the process was terminated by a signal.
def signal_exit? : Bool
{% if flag?(:unix) %}
# define __WIFSIGNALED(status) (((signed char) (((status) & 0x7f) + 1) >> 1) > 0)
((LibC::SChar.new(@exit_status & 0x7f) + 1) >> 1) > 0
0x01 <= (@exit_status & 0x7F) <= 0x7E
{% else %}
false
{% end %}
Expand All @@ -41,20 +179,13 @@ class Process::Status
#
# Available only on Unix-like operating systems.
def exit_signal : Signal
{% if flag?(:unix) %}
{% if flag?(:unix) && !flag?(:wasm32) %}
Signal.from_value(signal_code)
{% else %}
raise NotImplementedError.new("Process::Status#exit_signal")
{% end %}
end

{% if flag?(:wasm32) %}
# wasm32 does not define `Signal`
def exit_signal
raise NotImplementedError.new("Process::Status#exit_signal")
end
{% end %}

# If `normal_exit?` is `true`, returns the exit code of the process.
def exit_code : Int32
{% if flag?(:unix) %}
Expand Down