Class: Backticks::Command
- Inherits:
-
Object
- Object
- Backticks::Command
- Defined in:
- lib/backticks/command.rb
Overview
Represents a running process; provides mechanisms for capturing the process’s output, passing input, waiting for the process to end, and learning its 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.
Constant Summary collapse
- FOREVER =
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!
86_400 * 365
- CHUNK =
Number of bytes to read from the command in one “chunk”.
1_024
Instance Attribute Summary collapse
-
#captured_error ⇒ String
readonly
All output to stderr that has been captured so far.
-
#captured_input ⇒ String
readonly
All input that has been captured so far.
-
#captured_output ⇒ String
readonly
All output that has been captured so far.
-
#pid ⇒ Integer
readonly
Child process ID.
-
#status ⇒ nil, Process::Status
readonly
Result of command if it has ended; nil if still running.
Instance Method Summary collapse
-
#capture(limit = nil) ⇒ String?
Block until one of the following happens: - the command produces fresh output on stdout or stderr - the user passes some input to the command (if interactive) - the process exits - the time limit elapses (if provided) OR 60 seconds pass.
-
#eof? ⇒ Boolean
Determine whether output has been exhausted.
-
#initialize(pid, stdin, stdout, stderr, interactive: false) ⇒ Command
constructor
Watch a running command by taking ownership of the IO objects that are passed in.
-
#interactive? ⇒ Boolean
True if this command is tied to STDIN/STDOUT.
-
#join(limit = FOREVER) ⇒ Object
Block until the command exits, or until limit seconds have passed.
-
#success? ⇒ Boolean
Block until the command completes; return true if its status was zero, false if nonzero.
-
#tap {|stream, data| ... } ⇒ Object
Provide a callback to monitor input and output in real time.
-
#to_s ⇒ String
A basic string representation of this command.
Constructor Details
#initialize(pid, stdin, stdout, stderr, interactive: false) ⇒ Command
Watch a running command by taking ownership of the IO objects that are passed in.
40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/backticks/command.rb', line 40 def initialize(pid, stdin, stdout, stderr, interactive:false) @pid = pid @stdin = stdin @stdout = stdout @stderr = stderr @interactive = !!interactive @tap = nil @status = nil @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 |
Instance Attribute Details
#captured_error ⇒ String (readonly)
Returns all output to stderr that has been captured so far.
31 32 33 |
# File 'lib/backticks/command.rb', line 31 def captured_error @captured_error end |
#captured_input ⇒ String (readonly)
Returns all input that has been captured so far.
25 26 27 |
# File 'lib/backticks/command.rb', line 25 def captured_input @captured_input end |
#captured_output ⇒ String (readonly)
Returns all output that has been captured so far.
28 29 30 |
# File 'lib/backticks/command.rb', line 28 def captured_output @captured_output end |
#pid ⇒ Integer (readonly)
Returns child process ID.
19 20 21 |
# File 'lib/backticks/command.rb', line 19 def pid @pid end |
#status ⇒ nil, Process::Status (readonly)
Returns result of command if it has ended; nil if still running.
22 23 24 |
# File 'lib/backticks/command.rb', line 22 def status @status end |
Instance Method Details
#capture(limit = nil) ⇒ String?
Block until one of the following happens:
- the command produces fresh output on stdout or stderr
- the user passes some input to the command (if interactive)
- the process exits
- the time limit elapses (if provided) OR 60 seconds pass
Return up to CHUNK bytes of fresh output from the process, or return nil if no fresh output was produced
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/backticks/command.rb', line 132 def capture(limit=nil) streams = [@stdout, @stderr] streams << STDIN if STDIN.tty? && interactive? ready, _, _ = IO.select(streams, [], [], limit) # proxy STDIN to child's stdin if ready && ready.include?(STDIN) data = STDIN.readpartial(CHUNK) rescue nil if data data = @tap.call(:stdin, data) if @tap if data @captured_input << data @stdin.write(data) end else @tap.call(:stdin, nil) if @tap # our own STDIN got closed; proxy this fact to the child @stdin.close unless @stdin.closed? end end # capture child's stdout and maybe proxy to STDOUT if ready && ready.include?(@stdout) data = @stdout.readpartial(CHUNK) rescue nil if data data = @tap.call(:stdout, data) if @tap if data @captured_output << data STDOUT.write(data) if interactive? fresh_output = data end end end # capture child's stderr and maybe proxy to STDERR if ready && ready.include?(@stderr) data = @stderr.readpartial(CHUNK) rescue nil if data data = @tap.call(:stderr, data) if @tap if data @captured_error << data STDERR.write(data) if interactive? end end end fresh_output rescue Interrupt # Proxy Ctrl+C to the child (Process.kill('INT', @pid) rescue nil) if @interactive raise end |
#eof? ⇒ Boolean
Determine whether output has been exhausted.
74 75 76 77 78 79 80 81 |
# File 'lib/backticks/command.rb', line 74 def eof? @stdout.eof? && @stderr.eof? rescue Errno::EIO # The result of read operation when pty slave is closed is platform # dependent. # @see https://stackoverflow.com/questions/10238298/ruby-on-linux-pty-goes-away-without-eof-raises-errnoeio true end |
#interactive? ⇒ Boolean
Returns true if this command is tied to STDIN/STDOUT.
60 61 62 |
# File 'lib/backticks/command.rb', line 60 def interactive? @interactive end |
#join(limit = FOREVER) ⇒ Object
Block until the command exits, or until limit seconds have passed. If 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.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/backticks/command.rb', line 104 def join(limit=FOREVER) return self if @status tf = Time.now + limit until (t = Time.now) >= tf capture(tf-t) res = Process.waitpid(@pid, Process::WNOHANG) if res @status = $? capture(nil) until eof? return self end end return nil end |
#success? ⇒ Boolean
Block until the command completes; return true if its status was zero, false if nonzero.
68 69 70 71 |
# File 'lib/backticks/command.rb', line 68 def success? join status.success? end |
#tap {|stream, data| ... } ⇒ Object
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.
90 91 92 93 |
# File 'lib/backticks/command.rb', line 90 def tap(&block) raise StandardError.new("Tap is already set (#{@tap}); cannot set twice") if @tap && @tap != block @tap = block end |
#to_s ⇒ String
Returns a basic string representation of this command.
55 56 57 |
# File 'lib/backticks/command.rb', line 55 def to_s "#<Backticks::Command(@pid=#{pid},@status=#{@status || 'nil'})>" end |