{% if flag?(:win32) %}
require "c/ntstatus"
{% end %}
# The reason why a process terminated.
#
# 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 due to an abort request.
#
# * On Unix-like systems, this corresponds to `Signal::ABRT`, `Signal::KILL`,
# and `Signal::QUIT`.
# * 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 Ctrl + C and
# Ctrl + Break 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
# The process exited due to the user closing the terminal window or ending an ssh session.
#
# * On Unix-like systems, this corresponds to `Signal::HUP`.
# * On Windows, this corresponds to the `CTRL_CLOSE_EVENT` message.
TerminalDisconnected
# The process exited due to the user logging off or shutting down the OS.
#
# * On Unix-like systems, this corresponds to `Signal::TERM`.
# * On Windows, this corresponds to the `CTRL_LOGOFF_EVENT` and `CTRL_SHUTDOWN_EVENT` messages.
SessionEnded
# Returns `true` if the process exited abnormally.
#
# This includes all values except `Normal`.
def abnormal? : Bool
!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 : String
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`.
class Process::Status
# Platform-specific exit status code, which usually contains either the exit code or a termination signal.
# The other `Process::Status` methods extract the values from `exit_status`.
@[Deprecated("Use `#exit_reason`, `#exit_code`, or `#system_exit_status` instead")]
def exit_status : Int32
@exit_status.to_i32!
end
# Returns the exit status as indicated by the operating system.
#
# It can encode exit codes and termination signals and is platform-specific.
def system_exit_status : UInt32
@exit_status.to_u32!
end
{% if flag?(:win32) %}
# :nodoc:
def initialize(@exit_status : UInt32)
end
{% else %}
# :nodoc:
def initialize(@exit_status : Int32)
end
{% end %}
# Returns a platform-independent reason why 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) %}
case exit_signal?
when Nil
ExitReason::Normal
when .abrt?, .kill?, .quit?
ExitReason::Aborted
when .hup?
ExitReason::TerminalDisconnected
when .term?
ExitReason::SessionEnded
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
# TODO: stop / continue
ExitReason::Signal
end
{% else %}
raise NotImplementedError.new("Process::Status#exit_reason")
{% end %}
end
# Returns `true` if the process was terminated by a signal.
#
# NOTE: In contrast to `WIFSIGNALED` in glibc, the status code `0x7E` (`SIGSTOP`)
# is considered a signal.
#
# * `#abnormal_exit?` is a more portable alternative.
# * `#exit_signal?` provides more information about the signal.
def signal_exit? : Bool
!!exit_signal?
end
# Returns `true` if the process terminated normally.
#
# Equivalent to `ExitReason::Normal`
#
# * `#exit_reason` provides more insights into other exit reasons.
# * `#abnormal_exit?` returns the inverse.
def normal_exit? : Bool
exit_reason.normal?
end
# Returns `true` if the process terminated abnormally.
#
# Equivalent to `ExitReason#abnormal?`
#
# * `#exit_reason` provides more insights into the specific exit reason.
# * `#normal_exit?` returns the inverse.
def abnormal_exit? : Bool
exit_reason.abnormal?
end
# If `signal_exit?` is `true`, returns the *Signal* the process
# received and didn't handle. Will raise if `signal_exit?` is `false`.
#
# Available only on Unix-like operating systems.
#
# NOTE: `#exit_reason` is preferred over this method as a portable alternative
# which also works on Windows.
@[Deprecated("Use `#exit_signal?` instead.")]
def exit_signal : Signal
{% if flag?(:unix) && !flag?(:wasm32) %}
Signal.new(signal_code)
{% else %}
raise NotImplementedError.new("Process::Status#exit_signal")
{% end %}
end
# Returns the exit `Signal` or `nil` if there is none.
#
# On Windows returns always `nil`.
#
# * `#exit_reason` is a portable alternative.
def exit_signal? : Signal?
{% if flag?(:unix) && !flag?(:wasm32) %}
code = signal_code
unless code.zero?
Signal.new(code)
end
{% end %}
end
# Returns the exit code of the process if it exited normally (`#normal_exit?`).
#
# Raises `RuntimeError` if the status describes an abnormal exit.
#
# ```
# Process.run("true").exit_code # => 0
# Process.run("exit 123", shell: true).exit_code # => 123
# Process.new("sleep", ["10"]).tap(&.terminate).wait.exit_code # RuntimeError: Abnormal exit has no exit code
# ```
def exit_code : Int32
exit_code? || raise RuntimeError.new("Abnormal exit has no exit code")
end
# Returns the exit code of the process if it exited normally.
#
# Returns `nil` if the status describes an abnormal exit.
#
# ```
# Process.run("true").exit_code? # => 0
# Process.run("exit 123", shell: true).exit_code? # => 123
# Process.new("sleep", ["10"]).tap(&.terminate).wait.exit_code? # => nil
# ```
def exit_code? : Int32?
return unless normal_exit?
{% if flag?(:unix) %}
# define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
(@exit_status & 0xff00) >> 8
{% else %}
@exit_status.to_i32!
{% end %}
end
# Returns `true` if the process exited normally with an exit code of `0`.
def success? : Bool
exit_code? == 0
end
private def signal_code
# define __WTERMSIG(status) ((status) & 0x7f)
@exit_status & 0x7f
end
def_equals_and_hash @exit_status
# Prints a textual representation of the process status to *io*.
#
# The result is similar to `#to_s`, but prefixed by the type name,
# delimited by square brackets, and constants use full paths:
# `Process::Status[0]`, `Process::Status[1]`, `Process::Status[Signal::HUP]`,
# `Process::Status[LibC::STATUS_CONTROL_C_EXIT]`.
def inspect(io : IO) : Nil
io << "Process::Status["
{% if flag?(:win32) %}
if name = name_for_win32_exit_status
io << "LibC::" << name
else
stringify_exit_status_windows(io)
end
{% else %}
if signal = exit_signal?
signal.inspect(io)
else
exit_code.inspect(io)
end
{% end %}
io << "]"
end
private def name_for_win32_exit_status
case @exit_status
# Ignoring LibC::STATUS_SUCCESS here because we prefer its numerical representation `0`
when LibC::STATUS_FATAL_APP_EXIT then "STATUS_FATAL_APP_EXIT"
when LibC::STATUS_DATATYPE_MISALIGNMENT then "STATUS_DATATYPE_MISALIGNMENT"
when LibC::STATUS_BREAKPOINT then "STATUS_BREAKPOINT"
when LibC::STATUS_ACCESS_VIOLATION then "STATUS_ACCESS_VIOLATION"
when LibC::STATUS_ILLEGAL_INSTRUCTION then "STATUS_ILLEGAL_INSTRUCTION"
when LibC::STATUS_FLOAT_DIVIDE_BY_ZERO then "STATUS_FLOAT_DIVIDE_BY_ZERO"
when LibC::STATUS_FLOAT_INEXACT_RESULT then "STATUS_FLOAT_INEXACT_RESULT"
when LibC::STATUS_FLOAT_INVALID_OPERATION then "STATUS_FLOAT_INVALID_OPERATION"
when LibC::STATUS_FLOAT_OVERFLOW then "STATUS_FLOAT_OVERFLOW"
when LibC::STATUS_FLOAT_UNDERFLOW then "STATUS_FLOAT_UNDERFLOW"
when LibC::STATUS_PRIVILEGED_INSTRUCTION then "STATUS_PRIVILEGED_INSTRUCTION"
when LibC::STATUS_STACK_OVERFLOW then "STATUS_STACK_OVERFLOW"
when LibC::STATUS_CANCELLED then "STATUS_CANCELLED"
when LibC::STATUS_CONTROL_C_EXIT then "STATUS_CONTROL_C_EXIT"
end
end
# Prints a textual representation of the process status to *io*.
#
# A normal exit status prints the numerical value (`0`, `1` etc) or a named
# status (e.g. `STATUS_CONTROL_C_EXIT` on Windows).
# A signal exit status prints the name of the `Signal` member (`HUP`, `INT`, etc.).
def to_s(io : IO) : Nil
{% if flag?(:win32) %}
if name = name_for_win32_exit_status
io << name
else
stringify_exit_status_windows(io)
end
{% else %}
if signal = exit_signal?
if name = signal.member_name
io << name
else
signal.inspect(io)
end
else
io << exit_code
end
{% end %}
end
# Returns a textual representation of the process status.
#
# A normal exit status prints the numerical value (`0`, `1` etc) or a named
# status (e.g. `STATUS_CONTROL_C_EXIT` on Windows).
# A signal exit status prints the name of the `Signal` member (`HUP`, `INT`, etc.).
def to_s : String
{% if flag?(:win32) %}
name_for_win32_exit_status || String.build { |io| stringify_exit_status_windows(io) }
{% else %}
if signal = exit_signal?
signal.member_name || signal.inspect
else
exit_code.to_s
end
{% 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 : String
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
io << "0x"
@exit_status.to_s(base: 16, upcase: true).rjust(io, 8, '0')
else
@exit_status.to_s(io)
end
end
end