lib/backticks/command.rb in backticks-1.0.0rc1 vs lib/backticks/command.rb in backticks-1.0.0rc2

- old
+ new

@@ -4,17 +4,15 @@ # exitstatus. # # Interactive commands print their output to Ruby's STDOUT and STDERR # in realtime, and also pass input from Ruby's STDIN to the command's stdin. class Command - # Time value that is used internally when a user is willing to wait - # "forever" for the command. - # - # Using a definite time-value helps simplify the looping logic internally, - # but it does mean that this class will stop working in February of 2106. - # You have been warned! - FOREVER = Time.at(2**32-1).freeze + # Duration that we use when a caller is willing to wait "forever" for + # a command to finish. This means that `#join` is buggy when used with + # commands that take longer than a year to complete. You have been + # warned! + FOREVER = 86_400 * 365 # Number of bytes to read from the command in one "chunk". CHUNK = 1_024 # @return [Integer] child process ID @@ -30,56 +28,75 @@ attr_reader :captured_output # @return [String] all output to stderr that has been captured so far attr_reader :captured_error - # Watch a running command. - def initialize(pid, stdin, stdout, stderr) + # Watch a running command by taking ownership of the IO objects that + # are passed in. + # + # @param [Integer] pid + # @param [IO] stdin + # @param [IO] stdout + # @param [IO] stderr + def initialize(pid, stdin, stdout, stderr, interactive:false) @pid = pid @stdin = stdin @stdout = stdout @stderr = stderr + @interactive = !!interactive @captured_input = String.new.force_encoding(Encoding::BINARY) @captured_output = String.new.force_encoding(Encoding::BINARY) @captured_error = String.new.force_encoding(Encoding::BINARY) end - # @return [String] + # @return [String] a basic string representation of this command def to_s "#<Backticks::Command(@pid=#{pid},@status=#{@status || 'nil'})>" end + # @return [Boolean] true if this command is tied to STDIN/STDOUT def interactive? - !@stdin.nil? + @interactive end - # Provide a callback to monitor input and output in real time. + # Block until the command completes; return true if its status + # was zero, false if nonzero. + # + # @return [Boolean] + def success? + join + status.success? + end + + # Provide a callback to monitor input and output in real time. This method + # saves a reference to block for later use; whenever the command generates + # output or receives input, the block is called back with the name of the + # stream on which I/O occurred and the actual data that was read or written. # @yield - # @yieldparam + # @yieldparam [Symbol] stream one of :stdin, :stdout or :stderr + # @yieldparam [String] data fresh input from the designated stream def tap(&block) raise StandardError.new("Tap is already set (#{@tap}); cannot set twice") if @tap && @tap != block @tap = block end # Block until the command exits, or until limit seconds have passed. If - # interactive is true, pass user input to the command and print its output - # to Ruby's output streams. If the time limit expires, return `nil`; - # otherwise, return self. + # interactive is true, proxy STDIN to the command and print its output + # to STDOUT. If the time limit expires, return `nil`; otherwise, return + # self. # + # If the command has already exited when this method is called, return + # self immediately. + # # @param [Float,Integer] limit number of seconds to wait before returning - def join(limit=nil) + def join(limit=FOREVER) return self if @status - if limit - tf = Time.now + limit - else - tf = FOREVER - end - + tf = Time.now + limit until (t = Time.now) >= tf - capture(tf - t) + capture(tf-t) res = Process.waitpid(@pid, Process::WNOHANG) if res @status = $? return self end @@ -101,16 +118,10 @@ # @return [String,nil] fresh bytes from stdout/stderr, or nil if no output def capture(limit=nil) streams = [@stdout, @stderr] streams << STDIN if interactive? - if limit - tf = Time.now + limit - else - tf = FOREVER - end - - ready, _, _ = IO.select(streams, [], [], 0) + ready, _, _ = IO.select(streams, [], [], limit) # proxy STDIN to child's stdin if ready && ready.include?(STDIN) data = STDIN.readpartial(CHUNK) rescue nil if data