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

Platform-independent process exit reasons #12284

Closed
HertzDevil opened this issue Jul 16, 2022 · 1 comment · Fixed by #13052
Closed

Platform-independent process exit reasons #12284

HertzDevil opened this issue Jul 16, 2022 · 1 comment · Fixed by #13052

Comments

@HertzDevil
Copy link
Contributor

Some programs need to determine how another process terminated. For example, crystal run does this after running the temporary executable:

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
else
STDERR.puts "Program received and didn't handle signal #{signal} (#{signal.value})"
end
else
STDERR.puts "Program exited abnormally, the cause is unknown"
end
exit 1
end

status is a Process::Status. On Unix-like systems, if status.signal_exit? && status.exit_signal.segv?, then the process exited because of a segmentation fault. Windows executables do not communicate POSIX signals through the exit status, so that wouldn't work there. Nonetheless, we can still detect segmentation faults on Windows as follows:

lib LibC
  # these constants are defined in `errhandlingapi.cr` but
  # they should actually be from `minwinbase.cr`
  EXCEPTION_ACCESS_VIOLATION = 0xC0000005_u32
  EXCEPTION_STACK_OVERFLOW   = 0xC00000FD_u32
end

status.exit_status.in?(LibC::EXCEPTION_ACCESS_VIOLATION, LibC::EXCEPTION_STACK_OVERFLOW)

(Minor note: this won't work on Crystal programs at the moment, because currently the crash handler does not preserve those exit statuses.)

Neither exit_status nor exit_signal is platform-independent, so we would like some abstraction that uses the former on Windows, but the latter on Unix-like systems. Additionally, segmentation faults are not the only way a process could terminate, and there are other mutually exclusive reasons that could all be determined from the exit status. This calls for an enum:

enum Process::ExitReason
  Normal
  Aborted
  Interrupted
  AccessViolation
  BadMemoryAccess
  BadInstruction
  PosixSignal # any reasons not covered above; unused on windows
  Unknown

  def abnormal?
    !normal? && !unknown?
  end
end

And this enum is returned from a new method, Process::Status#exit_reason:

{% if flag?(:win32) %}
  lib LibC
    # minwinbase.cr
    EXCEPTION_DATATYPE_MISALIGNMENT = 0x80000002_u32
    EXCEPTION_ILLEGAL_INSTRUCTION   = 0xC000001D_u32
    EXCEPTION_PRIV_INSTRUCTION      = 0xC0000096_u32
    CONTROL_C_EXIT                  = 0xC000013A_u32

    # winnt.cr
    STATUS_DATATYPE_MISALIGNMENT_ERROR = 0xC00002C5_u32
  end
{% end %}

struct Process::Status
  def exit_reason : ExitReason
    {% if flag?(:win32) %}
      case @exit_status
      when LibC::STATUS_FATAL_APP_EXIT
        ExitReason::Aborted
      when LibC::CONTROL_C_EXIT
        ExitReason::Interrupted
      when LibC::EXCEPTION_ACCESS_VIOLATION, LibC::EXCEPTION_STACK_OVERFLOW
        ExitReason::AccessViolation
      when LibC::EXCEPTION_DATATYPE_MISALIGNMENT, LibC::STATUS_DATATYPE_MISALIGNMENT_ERROR
        ExitReason::BadMemoryAccess
      when LibC::EXCEPTION_ILLEGAL_INSTRUCTION, LibC::EXCEPTION_PRIV_INSTRUCTION
        ExitReason::BadInstruction
      else
        @exit_status.bits_set?(0x40000000_u32) ? ExitReason::Aborted : ExitReason::Normal
      end
    {% else %}
      if normal_exit?
        ExitReason::Normal
      elsif signal_exit?
        case exit_signal
        when .abrt?, .hup?, .kill?, .quit?, .term?
          ExitReason::Aborted
        when .int?
          ExitReason::Interrupted
        when .segv?
          ExitReason::AccessViolation
        when .bus?
          ExitReason::BadMemoryAccess
        when .ill?
          ExitReason::BadInstruction
        else
          ExitReason::PosixSignal
        end
      else
        ExitReason::Unknown
      end
    {% end %}
  end
end

I would like to reach consensus on ExitReason and #exit_reason before moving on with a PR, because there might be subtle differences between the different POSIX signals that default to process termination.

See also #7339, #8381, and #9064.

@straight-shoota
Copy link
Member

#13052 defines ExitReason::Breakpoint and ExitReason::FloatException additional to the ones mentioned here.
I suppose it makes sense to have them and they seem to be clearly defined on all platforms.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants