From d6fef935efa24120749a0304078eca00ca189280 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Mon, 13 Apr 2020 23:42:38 +0200 Subject: [PATCH] Super-fancy refactor of Process::Status Backwards-compatible! Based on https://github.com/crystal-lang/crystal/issues/8381#issuecomment-612691909 --- src/crystal/system/process.cr | 2 +- src/crystal/system/unix/process.cr | 18 +++- src/crystal/system/win32/process.cr | 4 +- src/process.cr | 3 +- src/process/status.cr | 125 +++++++++++++++++----------- 5 files changed, 99 insertions(+), 53 deletions(-) diff --git a/src/crystal/system/process.cr b/src/crystal/system/process.cr index fca85f7a6c7e..238841cff228 100644 --- a/src/crystal/system/process.cr +++ b/src/crystal/system/process.cr @@ -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 diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index a06e89e95f0b..c283623e6961 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -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? diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr index e9c4788191f9..597440f09da3 100644 --- a/src/crystal/system/win32/process.cr +++ b/src/crystal/system/win32/process.cr @@ -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, "The process might still be active") end - exit_code + ::Process::Status::Exited.new(exit_code.to_i32!) end def exists? diff --git a/src/process.cr b/src/process.cr index 566e1fae5755..1c2048fc9128 100644 --- a/src/process.cr +++ b/src/process.cr @@ -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 @@ -303,7 +304,7 @@ class Process end @wait_count = 0 - Process::Status.new(@process_info.wait) + @process_info.wait ensure close end diff --git a/src/process/status.cr b/src/process/status.cr index 6524f7844a08..2b49aced2c34 100644 --- a/src/process/status.cr +++ b/src/process/status.cr @@ -1,33 +1,13 @@ # The status of a terminated process. Returned by `Process#wait`. -class Process::Status - {% 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. +abstract struct Process::Status + # Returns `Signaled` if the process was terminated by a signal, otherwise `nil`. 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 %} + self.as? Signaled 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 @@ -35,11 +15,7 @@ class Process::Status # # 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. @@ -47,31 +23,84 @@ class Process::Status # * 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) - def exit_code : Int32 - if !normal_exit? - return -signal_code - end - {% if flag?(:unix) %} - # define __WEXITSTATUS(status) (((status) & 0xff00) >> 8) - (@exit_status & 0xff00) >> 8 - {% else %} - exit_status - {% end %} - end + 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 + + 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 - private def signal_code - # define __WTERMSIG(status) ((status) & 0x7f) - @exit_status & 0x7f + struct Stopped < Status + # :nodoc: + def initialize(@stop_signal : Int32) + end + + getter stop_signal : Int32 + + def exit_code : Int32 + -0x7F + end end - # Platform-specific exit status code, which usually contains either the exit code or a termination signal. + 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 - @exit_status.to_i32! + 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