Class: Shipit::Command
- Inherits:
-
Object
- Object
- Shipit::Command
- Defined in:
- lib/shipit/command.rb
Defined Under Namespace
Classes: Failed
Constant Summary collapse
- MAX_READ =
64.kilobytes
- Error =
Class.new(StandardError)
- NotFound =
Class.new(Error)
- Denied =
Class.new(Error)
- TimedOut =
Class.new(Error)
- BASE_ENV =
unbundled_env.merge((ENV.keys - unbundled_env.keys).map { |k| [k, nil] }.to_h)
Instance Attribute Summary collapse
-
#args ⇒ Object
readonly
Returns the value of attribute args.
-
#chdir ⇒ Object
readonly
Returns the value of attribute chdir.
-
#env ⇒ Object
readonly
Returns the value of attribute env.
-
#out ⇒ Object
readonly
Returns the value of attribute out.
-
#pid ⇒ Object
readonly
Returns the value of attribute pid.
-
#timeout ⇒ Object
readonly
Returns the value of attribute timeout.
Instance Method Summary collapse
- #code ⇒ Object
- #exit_message ⇒ Object
-
#initialize(*args, chdir:, default_timeout: Shipit.default_inactivity_timeout, env: {}) ⇒ Command
constructor
A new instance of Command.
- #interpolate_environment_variables(argument) ⇒ Object
- #interpolated_arguments ⇒ Object
- #kill(sig) {|red("Sending SIG#{sig} to PID #{@pid}\n")| ... } ⇒ Object
- #kill_and_wait(sig, wait, &block) ⇒ Object
- #output_timed_out? ⇒ Boolean
- #parse_arguments(arguments) ⇒ Object
- #parse_timeout(timeout) ⇒ Object
- #read_stream(io) ⇒ Object
- #reap_child!(block: true) ⇒ Object
- #red(text) ⇒ Object
- #run ⇒ Object
- #run! ⇒ Object
- #running? ⇒ Boolean
- #signaled? ⇒ Boolean
- #start(&block) ⇒ Object
- #stream(&block) ⇒ Object
- #stream!(&block) ⇒ Object
- #success? ⇒ Boolean
- #terminate!(&block) ⇒ Object
- #termination_status ⇒ Object
- #timed_out? ⇒ Boolean
- #to_s ⇒ Object
- #touch_last_output_at ⇒ Object
- #unbundled_env ⇒ Object
- #with_timeout(new_timeout) ⇒ Object
- #yield_control ⇒ Object
Constructor Details
#initialize(*args, chdir:, default_timeout: Shipit.default_inactivity_timeout, env: {}) ⇒ Command
Returns a new instance of Command.
31 32 33 34 35 36 37 |
# File 'lib/shipit/command.rb', line 31 def initialize(*args, chdir:, default_timeout: Shipit.default_inactivity_timeout, env: {}) @args, = parse_arguments(args) @timeout = parse_timeout(['timeout'] || [:timeout]) || default_timeout @env = env.transform_values { |v| v&.to_s } @chdir = chdir.to_s @timed_out = false end |
Instance Attribute Details
#args ⇒ Object (readonly)
Returns the value of attribute args.
29 30 31 |
# File 'lib/shipit/command.rb', line 29 def args @args end |
#chdir ⇒ Object (readonly)
Returns the value of attribute chdir.
29 30 31 |
# File 'lib/shipit/command.rb', line 29 def chdir @chdir end |
#env ⇒ Object (readonly)
Returns the value of attribute env.
29 30 31 |
# File 'lib/shipit/command.rb', line 29 def env @env end |
#out ⇒ Object (readonly)
Returns the value of attribute out.
29 30 31 |
# File 'lib/shipit/command.rb', line 29 def out @out end |
#pid ⇒ Object (readonly)
Returns the value of attribute pid.
29 30 31 |
# File 'lib/shipit/command.rb', line 29 def pid @pid end |
#timeout ⇒ Object (readonly)
Returns the value of attribute timeout.
29 30 31 |
# File 'lib/shipit/command.rb', line 29 def timeout @timeout end |
Instance Method Details
#code ⇒ Object
246 247 248 |
# File 'lib/shipit/command.rb', line 246 def code @status&.exitstatus end |
#exit_message ⇒ Object
61 62 63 |
# File 'lib/shipit/command.rb', line 61 def "#{self} #{termination_status}" end |
#interpolate_environment_variables(argument) ⇒ Object
51 52 53 54 55 |
# File 'lib/shipit/command.rb', line 51 def interpolate_environment_variables(argument) return argument.map { |a| interpolate_environment_variables(a) } if argument.is_a?(Array) EnvironmentVariables.with(env).interpolate(argument) end |
#interpolated_arguments ⇒ Object
81 82 83 |
# File 'lib/shipit/command.rb', line 81 def interpolated_arguments interpolate_environment_variables(@args) end |
#kill(sig) {|red("Sending SIG#{sig} to PID #{@pid}\n")| ... } ⇒ Object
206 207 208 209 |
# File 'lib/shipit/command.rb', line 206 def kill(sig) yield red("Sending SIG#{sig} to PID #{@pid}\n") Process.kill(sig, @pid) end |
#kill_and_wait(sig, wait, &block) ⇒ Object
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/shipit/command.rb', line 183 def kill_and_wait(sig, wait, &block) retry_count = 5 kill(sig, &block) begin with_timeout(wait) do read_stream(@out, &block) end rescue TimedOut rescue Errno::EIO # EIO is somewhat expected on Linux: http://stackoverflow.com/a/10306782 # If we try to read the stream right after sending a signal, we often get an Errno::EIO. return true if reap_child!(block: false) # If we let the child a little bit of time, it solves it. retry_count -= 1 if retry_count > 0 sleep(0.05) retry end end reap_child!(block: false) true end |
#output_timed_out? ⇒ Boolean
138 139 140 141 |
# File 'lib/shipit/command.rb', line 138 def output_timed_out? @last_output_at ||= Time.now.to_i (@last_output_at + timeout) < Time.now.to_i end |
#parse_arguments(arguments) ⇒ Object
227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/shipit/command.rb', line 227 def parse_arguments(arguments) = {} args = arguments.flatten.map do |argument| case argument when Hash .merge!(argument.values.first) argument.keys.first else argument end end [args.map(&:to_s), ] end |
#parse_timeout(timeout) ⇒ Object
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/shipit/command.rb', line 211 def parse_timeout(timeout) case timeout when String begin Duration.parse(timeout).to_i rescue Duration::ParseError # If given garbage we fallback to the default. # It's not ideal but we don't have a good way to notify about # syntax errors. nil end else timeout end end |
#read_stream(io) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/shipit/command.rb', line 151 def read_stream(io) touch_last_output_at loop do yield_control yield io.read_nonblock(MAX_READ) touch_last_output_at rescue IO::WaitReadable if output_timed_out? @timed_out = true raise TimedOut end IO.select([io], nil, nil, 1) retry end rescue EOFError end |
#reap_child!(block: true) ⇒ Object
254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/shipit/command.rb', line 254 def reap_child!(block: true) return @status if @status return unless running? # Command was never started e.g. permission denied, not found etc if block _, @status = Process.waitpid2(@pid) elsif res = Process.waitpid2(@pid, Process::WNOHANG) @status = res[1] end @status end |
#red(text) ⇒ Object
123 124 125 |
# File 'lib/shipit/command.rb', line 123 def red(text) "\033[1;31m#{text}\033[0m" end |
#run ⇒ Object
65 66 67 68 69 70 71 |
# File 'lib/shipit/command.rb', line 65 def run output = [] stream do |out| output << out end output.join end |
#run! ⇒ Object
73 74 75 76 77 78 79 |
# File 'lib/shipit/command.rb', line 73 def run! output = [] stream! do |out| output << out end output.join end |
#running? ⇒ Boolean
242 243 244 |
# File 'lib/shipit/command.rb', line 242 def running? !!pid && !@status end |
#signaled? ⇒ Boolean
250 251 252 |
# File 'lib/shipit/command.rb', line 250 def signaled? @status.signaled? end |
#start(&block) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/shipit/command.rb', line 85 def start(&block) return if @started @control_block = block @out = @pid = nil FileUtils.mkdir_p(@chdir) begin @out, child_in, @pid = PTY.spawn(unbundled_env, *interpolated_arguments, chdir: @chdir) child_in.close rescue Errno::ENOENT raise NotFound, "#{Shellwords.split(interpolated_arguments.first).first}: command not found" rescue Errno::EACCES raise Denied, "#{Shellwords.split(interpolated_arguments.first).first}: Permission denied" end @started = true self end |
#stream(&block) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/shipit/command.rb', line 107 def stream(&block) start begin read_stream(@out, &block) rescue TimedOut => e yield "#{red("No output received in the last #{timeout} seconds.")}\n" terminate!(&block) raise e rescue Errno::EIO # Somewhat expected on Linux: http://stackoverflow.com/a/10306782 end self ensure reap_child! end |
#stream!(&block) ⇒ Object
127 128 129 130 131 132 |
# File 'lib/shipit/command.rb', line 127 def stream!(&block) stream(&block) raise Failed.new(, code) unless success? self end |
#success? ⇒ Boolean
57 58 59 |
# File 'lib/shipit/command.rb', line 57 def success? !code.nil? && code.zero? end |
#terminate!(&block) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/shipit/command.rb', line 168 def terminate!(&block) kill_and_wait('INT', 5, &block) || kill_and_wait('INT', 2, &block) || kill_and_wait('TERM', 5, &block) || kill_and_wait('TERM', 2, &block) || kill('KILL', &block) rescue Errno::ECHILD, Errno::ESRCH true # much success ensure begin read_stream(@out, &block) rescue StandardError end end |
#termination_status ⇒ Object
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/shipit/command.rb', line 266 def termination_status if running? "is running" elsif success? "terminated successfully" elsif timed_out? && signaled? "timed out and terminated with #{Signal.signame(@status.termsig)} signal" elsif timed_out? "timed out and terminated with exit status #{exitstatus}" elsif signaled? "terminated with #{Signal.signame(@status.termsig)} signal" else "terminated with exit status #{code}" end end |
#timed_out? ⇒ Boolean
134 135 136 |
# File 'lib/shipit/command.rb', line 134 def timed_out? @timed_out end |
#to_s ⇒ Object
47 48 49 |
# File 'lib/shipit/command.rb', line 47 def to_s @args.join(' ') end |
#touch_last_output_at ⇒ Object
143 144 145 |
# File 'lib/shipit/command.rb', line 143 def touch_last_output_at @last_output_at = Time.now.to_i end |
#unbundled_env ⇒ Object
103 104 105 |
# File 'lib/shipit/command.rb', line 103 def unbundled_env BASE_ENV.merge('PATH' => "#{Shipit.shell_paths.join(':')}:#{ENV['PATH']}").merge(@env.stringify_keys) end |
#with_timeout(new_timeout) ⇒ Object
39 40 41 42 43 44 45 |
# File 'lib/shipit/command.rb', line 39 def with_timeout(new_timeout) old_timeout = timeout @timeout = new_timeout yield ensure @timeout = old_timeout end |
#yield_control ⇒ Object
147 148 149 |
# File 'lib/shipit/command.rb', line 147 def yield_control @control_block&.call end |