Class: Terraspace::Shell
- Inherits:
-
Object
- Object
- Terraspace::Shell
- Includes:
- Util::Logging
- Defined in:
- lib/terraspace/shell.rb,
lib/terraspace/shell/error.rb
Defined Under Namespace
Classes: Error
Constant Summary collapse
- BLOCK_SIZE =
Integer(ENV['TS_BUFFER_BLOCK_SIZE'] || 102400)
- BUFFER_TIMEOUT =
3600s = 1h
Integer(ENV['TS_BUFFER_TIMEOUT'] || 3600)
Instance Method Summary collapse
- #all_eof?(files) ⇒ Boolean
- #exit_status(status) ⇒ Object
- #handle_input(stdin, line) ⇒ Object
- #handle_stderr(line) ⇒ Object
- #handle_stdout(line, newline: true) ⇒ Object
- #handle_streams(stdin, stdout, stderr) ⇒ Object
-
#initialize(mod, command, options = {}) ⇒ Shell
constructor
A new instance of Shell.
- #popen3(env) ⇒ Object
-
#run ⇒ Object
requires @mod to be set quiet useful for RemoteState::Fetcher.
- #shell ⇒ Object
- #suppress_newline(line) ⇒ Object
- #system? ⇒ Boolean
Methods included from Util::Logging
Constructor Details
#initialize(mod, command, options = {}) ⇒ Shell
Returns a new instance of Shell.
7 8 9 |
# File 'lib/terraspace/shell.rb', line 7 def initialize(mod, command, ={}) @mod, @command, @options = mod, command, end |
Instance Method Details
#all_eof?(files) ⇒ Boolean
137 138 139 |
# File 'lib/terraspace/shell.rb', line 137 def all_eof?(files) files.find { |f| !f.eof }.nil? end |
#exit_status(status) ⇒ Object
141 142 143 144 145 146 147 148 149 150 |
# File 'lib/terraspace/shell.rb', line 141 def exit_status(status) return if status == 0 exit_on_fail = @options[:exit_on_fail].nil? ? true : @options[:exit_on_fail] if @error && @error.known? raise @error.instance elsif exit_on_fail raise ShellError.new("Error running command: #{@command}") end end |
#handle_input(stdin, line) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/terraspace/shell.rb', line 103 def handle_input(stdin, line) # Edge case: "value:" chopped off "Enter a" and "value" prompt # This means "Enter a value:" is not needed but leaving it for now patterns = [ /^ value:/, "Enter a value:", "\e[0m\e[1mvar.", # prompts for variable input. can happen on plan or apply. looking for bold marker also in case "var." shows up somewhere else ] matched = patterns.any? do |pattern| if pattern.is_a?(String) line.include?(pattern) else # Regexp line.match(pattern) end end if matched answer = $stdin.gets logger.stdin_capture(answer.strip) stdin.write_nonblock(answer) end end |
#handle_stderr(line) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/terraspace/shell.rb', line 125 def handle_stderr(line) @error ||= Error.new @error.lines << line # aggregate all error lines return if @error.known? # Sometimes may print a "\e[31m\n" which like during dependencies fetcher init # suppress it so dont get a bunch of annoying "newlines" return if line == "\e[31m\n" && @options[:suppress_error_color] logger.error(line) end |
#handle_stdout(line, newline: true) ⇒ Object
92 93 94 95 96 97 98 99 100 101 |
# File 'lib/terraspace/shell.rb', line 92 def handle_stdout(line, newline: true) # Terraspace logger has special stdout method so original terraform output # can be piped to jq. IE: # terraspace show demo --json | jq if logger.respond_to?(:stdout) && !@options[:log_to_stderr] logger.stdout(line, newline: newline) else logger.info(line) end end |
#handle_streams(stdin, stdout, stderr) ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/terraspace/shell.rb', line 50 def handle_streams(stdin, stdout, stderr) # note: t=0 and t=nil means no timeout. See: https://bit.ly/2PURlCX t = BUFFER_TIMEOUT.to_i unless BUFFER_TIMEOUT.nil? Timeout::timeout(t) do files = [stdout, stderr] until all_eof?(files) do ready = IO.select(files) next unless ready readable = ready[0] readable.each do |f| buffer = f.read_nonblock(BLOCK_SIZE, exception: false) next unless buffer lines = buffer.split("\n") lines.each do |line| if f.fileno == stdout.fileno handle_stdout(line, newline: !suppress_newline(line)) handle_input(stdin, line) else handle_stderr(line) end end end end end end |
#popen3(env) ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/terraspace/shell.rb', line 35 def popen3(env) Open3.popen3(env, @command, chdir: @mod.cache_dir) do |stdin, stdout, stderr, wait_thread| # interesting. simply handling the trap and doing nothing works # Think its because ctrl-c is sent to both processes. # 1. we do nothing in here in the parent process # 2. in the child process the ctrl-c gets sent directly to the terraform command Signal.trap("INT") { } handle_streams(stdin, stdout, stderr) status = wait_thread.value.exitstatus exit_status(status) end end |
#run ⇒ Object
requires @mod to be set quiet useful for RemoteState::Fetcher
13 14 15 16 17 18 |
# File 'lib/terraspace/shell.rb', line 13 def run msg = "=> #{@command}" @options[:quiet] ? logger.debug(msg) : logger.info(msg) return if ENV['TS_TEST'] shell end |
#shell ⇒ Object
20 21 22 23 24 25 26 27 28 |
# File 'lib/terraspace/shell.rb', line 20 def shell env = @options[:env] || {} env.stringify_keys! if system? system(env, @command, chdir: @mod.cache_dir) else popen3(env) end end |
#suppress_newline(line) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/terraspace/shell.rb', line 78 def suppress_newline(line) # Regular prompt line.include?("Enter a value:") || # Edge case: When buffer is very large buffer.split("\n") only gives 8192 chars at a time line.size == 8192 && line[-1] != "\n" || # Edge case: "value:" chopped off "Enter a" and "value" prompt # Very hard to reproduce. Happens 1/5 times on terraspace up autoscaling example. # Sometimes lines come in as: # [...," Only 'yes' will be accepted to approve.", "", " \e[1mEnter a"] # [" value:\e[0m \e[0m"] line.match(/Enter a$/) || line.match(/^ value:/) # chopped off prompt # line.include?(" value:") && lines.last.match(/Enter a$/) # chopped off prompt end |
#system? ⇒ Boolean
30 31 32 33 |
# File 'lib/terraspace/shell.rb', line 30 def system? @options[:shell] == "system" || # terraspace console ENV['TS_RUNNER_SYSTEM'] # allow manual override end |