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

Super-fancy refactor of Process::Status #9064

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion src/crystal/system/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct Crystal::System::Process
# def pid : Int

# Waits until the process finishes and returns its status code
# def wait : Int
# def wait : ::Process::Status

# Whether the process is still registered in the system.
# def exists? : Bool
Expand Down
18 changes: 17 additions & 1 deletion src/crystal/system/unix/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,23 @@ struct Crystal::System::Process
end

def wait
@channel.receive
status = @channel.receive
case status & 0xff
when 0
::Process::Status::Exited.new((status & 0xff00) >> 8)
when 0x01..0x7e
::Process::Status::Signaled.new(status & 0x7f, core_dumped: false)
when 0x7f
::Process::Status::Stopped.new((status & 0xff00) >> 8)
when 0x81..0xfe
::Process::Status::Signaled.new(status & 0x7f, core_dumped: true)
else
if status == 0xffff
::Process::Status::Continued.new
else
raise ::Process::Status::UnexpectedStatusError.new(status)
end
end
end

def exists?
Expand Down
4 changes: 2 additions & 2 deletions src/crystal/system/win32/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ struct Crystal::System::Process
raise RuntimeError.from_winerror("GetExitCodeProcess")
end
if exit_code == LibC::STILL_ACTIVE
raise "BUG: process still active"
raise ::Process::Status::UnexpectedStatusError.new(exit_code.to_i32!, "The process might still be active")
end
exit_code
::Process::Status::Exited.new(exit_code.to_i32!)
end

def exists?
Expand Down
3 changes: 2 additions & 1 deletion src/process.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Process
record Tms, utime : Float64, stime : Float64, cutime : Float64, cstime : Float64
end

require "process/status"
require "crystal/system/process"

class Process
Expand Down Expand Up @@ -303,7 +304,7 @@ class Process
end
@wait_count = 0

Process::Status.new(@process_info.wait)
@process_info.wait
ensure
close
end
Expand Down
136 changes: 86 additions & 50 deletions src/process/status.cr
Original file line number Diff line number Diff line change
@@ -1,70 +1,106 @@
# 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`.
def exit_status : Int32
@exit_status.to_i32!
abstract struct Process::Status
# Returns `Signaled` if the process was terminated by a signal, otherwise `nil`.
def signal_exit? : Signaled?
self.as? Signaled
end

{% if flag?(:win32) %}
# :nodoc:
def initialize(@exit_status : UInt32)
end
{% else %}
# :nodoc:
def initialize(@exit_status : Int32)
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
{% else %}
false
{% end %}
end

# Returns `true` if the process terminated normally.
def normal_exit? : Bool
{% if flag?(:unix) %}
# define __WIFEXITED(status) (__WTERMSIG(status) == 0)
signal_code == 0
{% else %}
true
{% end %}
# Returns `Exited` if the process terminated normally, otherwise `nil`.
def normal_exit? : Exited?
self.as? Exited
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.
def exit_signal : Signal
{% if flag?(:unix) %}
Signal.from_value(signal_code)
{% else %}
raise NotImplementedError.new("Process::Status#exit_signal")
{% end %}
self.as(Signaled).signal
end

# If `normal_exit?` is `true`, returns the exit code of the process.
def exit_code : Int32
{% if flag?(:unix) %}
# define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
(@exit_status & 0xff00) >> 8
{% else %}
exit_status
{% end %}
end
#
# * POSIX: Otherwise, returns a placeholder negative value related to the exit signal.
# * Windows: The exit is always "normal" but the exit codes can be negative
# (wrapped around after `Int32::MAX`; take them modulo 2**32 if the actual value is needed)
abstract def exit_code : Int32

# Returns `true` if the process exited normally with an exit code of `0`.
def success? : Bool
normal_exit? && exit_code == 0
false
end

struct Exited < Status
# :nodoc:
def initialize(@exit_code : Int32)
end

getter exit_code : Int32

def success? : Bool
exit_code == 0
end
end

private def signal_code
# define __WTERMSIG(status) ((status) & 0x7f)
@exit_status & 0x7f
struct Signaled < Status
# :nodoc:
def initialize(signal_code : Int32, @core_dumped : Bool)
@signal_code = UInt8.new(signal_code)
end

def signal : Signal
Signal.new(signal_code)
end

def signal_code : Int32
@signal_code.to_i32
end

def exit_code : Int32
-signal_code
end

getter? core_dumped : Bool
end

struct Stopped < Status
# :nodoc:
def initialize(@stop_signal : Int32)
end

getter stop_signal : Int32

def exit_code : Int32
-0x7F
end
end

struct Continued < Status
def exit_code : Int32
-0x7F
end
end

# POSIX-specific exit status code, a complex bitmask of an exit code or termination signal.
@[Deprecated("Use `Process::Status#exit_code`")]
def exit_status : Int32
case self
when Exited
self.exit_code << 8
when Signaled
self.signal_code + (core_dumped? ? 0x80 : 0)
when Stopped
(self.stop_signal << 8) + 0x7f
when Continued
0xffff
end
end

class UnexpectedStatusError < RuntimeError
def initialize(@exit_status : Int32, msg = "The process exited with an unknown status")
super("#{msg} (#{@exit_status})")
end

getter exit_status : Int32
end
end