Module: EasyIO

Defined in:
lib/easy_io/run.rb,
lib/easy_io/disk.rb,
lib/easy_io/config.rb,
lib/easy_io/logger.rb,
lib/easy_io/registry.rb,
lib/easy_io/terminal.rb

Defined Under Namespace

Modules: Disk, Registry, Terminal

Class Method Summary collapse

Class Method Details

._full_error_message(error_message, error_options, command) ⇒ Object



219
220
221
222
# File 'lib/easy_io/run.rb', line 219

def _full_error_message(error_message, error_options, command)
  command_message = error_options['show_command_on_error'] && command ? "\nCommand causing exception: " + command + "\n" : ''
  "Exception: #{error_message}\n#{error_options['info_on_exception']}#{'=' * 120}\n#{command_message}"
end

._parse_for_errors(message, error_messages, error_options, command) ⇒ Object



209
210
211
212
# File 'lib/easy_io/run.rb', line 209

def _parse_for_errors(message, error_messages, error_options, command)
  errors_found = error_options['regex_error_filters'].any? { |regex_filter| message =~ regex_filter }
  _process_error_message(message, error_messages, error_options, command) if errors_found
end

._process_error_message(error_message, error_messages, error_options, command) ⇒ Object



214
215
216
217
# File 'lib/easy_io/run.rb', line 214

def _process_error_message(error_message, error_messages, error_options, command)
  raise _full_error_message(error_message, error_options, command) if error_options['raise_on_first_error']
  error_messages.push(error_message) # if we're not raising right away, add to the list of errors
end

.add_as_winrm_trusted_host(remote_host) ⇒ Object



204
205
206
207
# File 'lib/easy_io/run.rb', line 204

def add_as_winrm_trusted_host(remote_host)
  trusted_hosts = EasyIO.powershell_out('(Get-Item WSMan:\localhost\Client\TrustedHosts).value', return_all_stdout: true)
  EasyIO.powershell_out("Set-Item WSMan:\\localhost\\Client\\TrustedHosts -Value 'trusted_hosts, #{remote_host}' -Force") unless trusted_hosts.include?(remote_host)
end

.configObject



4
5
6
# File 'lib/easy_io/config.rb', line 4

def config
  @config ||= EasyJSON.config(defaults: defaults)
end

.defaultsObject



8
9
10
11
12
13
14
# File 'lib/easy_io/config.rb', line 8

def defaults
  {
    'paths' => {
      'cache' => Dir.tmpdir,
    },
  }
end

.execute_out(command, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], powershell: false, show_command_on_error: false, raise_on_first_error: true, return_all_stdout: false, output_separator: nil) ⇒ Object

execute a command with real-time output. Any stdout you want returned to the caller must come after the :output_separator which defaults to ‘#return_data#:’

return_all_stdout: return all output to the caller instead after process completion


6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/easy_io/run.rb', line 6

def execute_out(command, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], powershell: false, show_command_on_error: false, raise_on_first_error: true, return_all_stdout: false, output_separator: nil)
  raise "Invalid argument to execute_out! working_folder (#{working_folder}) is not a valid directory!" unless ::File.directory?(working_folder)
  output_separator ||= '#return_data#:'
  if return_all_stdout
    result = ''
    return_data_flag = true
  else
    STDOUT.sync = true
    result = nil
    return_data_flag = false
  end
  exit_status = nil
  error_messages = []
  info_on_exception = "#{info_on_exception}\n" unless info_on_exception.end_with?("\n")
  error_options = { 'show_command_on_error' => show_command_on_error, 'info_on_exception' => info_on_exception, 'regex_error_filters' => regex_error_filters, 'raise_on_first_error' => raise_on_first_error }
  if powershell
    ps_script_file = "#{EasyIO.config['paths']['cache']}/easy_io/scripts/ps_script-thread_id-#{Thread.current.object_id}.ps1"
    FileUtils.mkdir_p(::File.dirname(ps_script_file)) unless ::File.directory? ::File.dirname(ps_script_file)
    ::File.write(ps_script_file, command)
  end

  popen_arguments = powershell ? ['powershell.exe', ps_script_file] : [command]
  Open3.popen3(*popen_arguments, chdir: working_folder) do |_stdin, stdout, stderr, wait_thread|
    unless pid_logfile.nil? # Log pid in case job or script dies
      FileUtils.mkdir_p(::File.dirname(pid_logfile)) unless ::File.directory? ::File.dirname(pid_logfile)
      ::File.write(pid_logfile, wait_thread.pid)
    end
    buffers = [stdout, stderr]
    queued_buffers = IO.select(buffers) || [[]]
    queued_buffers.first.each do |buffer|
      case buffer
      when stdout
        while (line = buffer.gets)
          if return_data_flag
            result += line
            next
          end
          stdout_split = line.split(output_separator)
          stdout_message = stdout_split.first.strip
          _parse_for_errors(stdout_message, error_messages, error_options, command)
          EasyIO.logger.info stdout_message unless stdout_message.empty?
          if stdout_split.count > 1
            return_data_flag = true
            result = stdout_split.last
          end
        end
      when stderr
        error_message = ''
        error_message += line while (line = buffer.gets)
        next if error_message.empty?
        if exception_exceptions.any? { |ignore_filter| error_message =~ ignore_filter }
          EasyIO.logger.info error_message.strip
          next
        end
        _process_error_message(error_message, error_messages, error_options, command)
      end
    end
    exit_status = wait_thread.value
  end
  unless error_messages.empty?
    last_error = _full_error_message(error_messages.pop, error_options, command)
    error_messages.map! { |error_message| _full_error_message(error_message, error_options, nil) }
    error_messages.push(last_error)
    raise error_messages.join("\n")
  end
  [result, exit_status]
end

.levelsObject



44
45
46
47
48
49
50
51
52
53
# File 'lib/easy_io/logger.rb', line 44

def levels
  {
    'info' => Logger::INFO,
    'debug' => Logger::DEBUG,
    'warn' => Logger::WARN,
    'error' => Logger::ERROR,
    'fatal' => Logger::FATAL,
    'unknown' => Logger::UNKNOWN,
  }
end

.loggerObject



40
41
42
# File 'lib/easy_io/logger.rb', line 40

def logger
  @logger
end

.logger=(value) ⇒ Object

For portability, can be overridden with a class that has methods :level, :fatal, :error, :warn, :info, :debug and the others specified below. See ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html

For example, when using with Chef, set the logger to Chef::Log



35
36
37
38
# File 'lib/easy_io/logger.rb', line 35

def logger=(value)
  @logger = value
  @logger.class.class_eval { include LoggerEnhancement }
end

.notepad_prompt(text_file_path, comment) ⇒ Object



232
233
234
235
236
237
238
239
# File 'lib/easy_io/run.rb', line 232

def notepad_prompt(text_file_path, comment)
  ::FileUtils.mkdir_p ::File.dirname(text_file_path) unless ::File.directory?(::File.dirname(text_file_path))
  ::File.write(text_file_path, "; #{comment}") unless ::File.exist?(text_file_path)
  EasyIO.logger.info comment.gsub('here', 'in the notepad window')
  `notepad #{text_file_path}`
  notepad_content = ::File.read(text_file_path)
  notepad_content.gsub(/;[^\r\n]*(\r\n|\r|\n)/i, '') # remove comments in text file
end

.pid_running?(pid) ⇒ Boolean

Returns:

  • (Boolean)


224
225
226
227
228
229
230
# File 'lib/easy_io/run.rb', line 224

def pid_running?(pid)
  begin
    Process.kill(0, pid) # Does not actually kill process, checks if it's running.
  rescue Errno::ESRCH
    nil
  end == 1
end

.powershell_out(ps_script, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], show_command_on_error: false, return_all_stdout: false, output_separator: nil) ⇒ Object

execute a powershell script with real-time output. Any stdout you want returned to the caller must come after the :output_separator which defaults to ‘#return_data#:’

return_all_stdout: return all output to the caller instead after process completion


76
77
78
# File 'lib/easy_io/run.rb', line 76

def powershell_out(ps_script, pid_logfile: nil, working_folder: Dir.pwd, regex_error_filters: [], info_on_exception: '', exception_exceptions: [], show_command_on_error: false, return_all_stdout: false, output_separator: nil)
  execute_out(ps_script, pid_logfile: pid_logfile, working_folder: working_folder, regex_error_filters: regex_error_filters, info_on_exception: info_on_exception, exception_exceptions: exception_exceptions, powershell: true, show_command_on_error: show_command_on_error, return_all_stdout: return_all_stdout, output_separator: output_separator)
end

.process_command_output(data, output_stream, output_to_terminal) ⇒ Object



161
162
163
164
# File 'lib/easy_io/run.rb', line 161

def process_command_output(data, output_stream, output_to_terminal)
  output_stream << data if output_stream
  EasyIO.logger.info data if output_to_terminal
end

.run_command_on_remote_hosts(remote_hosts, command, credentials, command_message: nil, shell_type: nil, tail_count: nil, set_as_trusted_host: false, transport: :ssh) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/easy_io/run.rb', line 166

def run_command_on_remote_hosts(remote_hosts, command, credentials, command_message: nil, shell_type: nil, tail_count: nil, set_as_trusted_host: false, transport: :ssh)
  tail_count ||= 1 # Return the last (1) line from each remote_host's log to the console
  shell_type ||= OS.windows? ? :cmd : :bash
  shell_type = shell_type.to_sym unless shell_type.is_a?(Symbol)
  supported_shell_types = OS.windows? ? [:cmd, :powershell] : [:bash]
  raise "Unsupported shell_type for running remote commands: '#{shell_type}'" unless supported_shell_types.include?(shell_type)

  threads = {}
  threads_output = {}
  log_folder = "#{EasyIO.config['paths']['cache']}/easy_io/logs"
  ::FileUtils.mkdir_p log_folder unless ::File.directory?(log_folder)
  EasyIO.logger.info "Output logs of processes run on the specified remote hosts will be placed in #{log_folder}..."
  remote_hosts.each do |remote_host|
    EasyIO.logger.info "Running `#{command_message || command}` on #{remote_host}..."
    threads[remote_host] = Thread.new do
      case shell_type
      when :powershell
        threads_output[remote_host] = run_remote_powershell_command(remote_host, command, credentials, set_as_trusted_host: set_as_trusted_host, transport: transport)
      when :cmd, :bash
        threads_output[remote_host] = run_remote_command(remote_host, command, credentials)
      end
    end
  end
  threads.values.each(&:join) # Wait for all commands to complete
  exceptions = []
  threads_output.each do |remote_host, output|
    ::File.write("#{log_folder}/#{EasyFormat::File.windows_friendly_name(remote_host)}.#{::Time.now.strftime('%Y%m%d_%H%M%S')}.log", "#{output['stdout']}\n#{output['stderr']}")
    tail_output = output['stdout'].nil? ? '--no standard output--' : output['stdout'].split("\n").last(tail_count).join("\n")
    EasyIO.logger.info "[#{remote_host}]: #{tail_output}"
    if output['exception']
      exceptions.push "Failed to run command on #{remote_host}: #{output['stderr']}\n#{output['exception'].cause}\n#{output['exception'].message}"
      next
    end
    exceptions.push "The script exited with exit code #{output['exitcode']}.\n\n#{output['stderr']}" unless output['exitcode'] == 0
  end
  raise exceptions.join("\n\n") unless exceptions.empty?
end

.run_remote_command(remote_host, command, credentials, ssh_key: nil, output_stream: nil, output_file: nil, output_to_terminal: false, show_command_on_error: false) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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
# File 'lib/easy_io/run.rb', line 107

def run_remote_command(remote_host, command, credentials, ssh_key: nil, output_stream: nil, output_file: nil, output_to_terminal: false, show_command_on_error: false)
  require 'net/ssh'
  start_params = if credentials['password'].nil?
                   {
                     host_key: 'ssh-rsa',
                     keys: [ssh_key],
                     verify_host_key: false,
                   }
                 else
                   {
                     password: credentials['password'],
                   }
                 end

  retries ||= 3
  return_code = nil
  keep_stream_open = !output_stream.nil? # Leave the stream open if it was passed in
  output_stream ||= ::File.open(output_file, ::File::RDWR | ::File::CREAT) unless output_file.nil?
  stdout = ''

  Net::SSH.start(remote_host, credentials['user'], **start_params) do |ssh|
    command_thread = ssh.open_channel do |channel|
      channel.exec(command) do |ch, success|
        raise 'could not execute command' unless success

        ch.on_data { |_c, data| stdout += data; process_command_output(data, output_stream, output_to_terminal) }
        ch.on_extended_data { |_c, _type, data| stdout += data; process_command_output(data, output_stream, output_to_terminal) }
        ch.on_request('exit-status') { |_ch, data| return_code = data.read_long }
      end
    end
    command_thread.wait
  end
  {
    'stdout' => stdout,
    'exitcode' => return_code,
  }
rescue Net::SSH::ConnectionTimeout => ex
  EasyIO.logger.info 'Net::SSH::ConnectionTimeout - retrying...'
  retry if (retries -= 1) >= 0
  {
    'exception' => ex,
    'stderr' => (show_command_on_error ? "command: #{command}\n\n#{ex.message}" : ex.message) + "\n\n#{ex.backtrace}",
    'exitcode' => return_code,
  }
rescue => ex
  {
    'exception' => ex,
    'stderr' => (show_command_on_error ? "command: #{command}\n\n#{ex.message}" : ex.message) + "\n\n#{ex.backtrace}",
    'exitcode' => return_code,
  }
ensure
  output_stream.close unless keep_stream_open || !output_stream.respond_to?(:close)
end

.run_remote_powershell_command(remote_host, command, credentials, set_as_trusted_host: false, transport: :ssh, output_stream: nil, output_file: nil, output_to_terminal: false) ⇒ Object



101
102
103
104
105
# File 'lib/easy_io/run.rb', line 101

def run_remote_powershell_command(remote_host, command, credentials, set_as_trusted_host: false, transport: :ssh, output_stream: nil, output_file: nil, output_to_terminal: false)
  return run_remote_winrm_command(remote_host, command, credentials, set_as_trusted_host: set_as_trusted_host) if transport == :winrm
  command = 'powershell.exe ' unless command =~ /powershell/i
  run_remote_command(remote_host, command, credentials, output_stream: output_stream, output_file: output_file, output_to_terminal: output_to_terminal)
end

.run_remote_winrm_command(remote_host, command, credentials, set_as_trusted_host: false) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/easy_io/run.rb', line 80

def run_remote_winrm_command(remote_host, command, credentials, set_as_trusted_host: false)
  add_as_winrm_trusted_host(remote_host) if set_as_trusted_host

  remote_command = <<-EOS
    $securePassword = ConvertTo-SecureString -AsPlainText '#{credentials['password']}' -Force
    $credential = New-Object System.Management.Automation.PSCredential -ArgumentList #{credentials['user']}, $securePassword
    Invoke-Command -ComputerName #{remote_host} -Credential $credential -ScriptBlock { #{command} }
  EOS
  output = powershell_out(remote_command, return_all_stdout: true)
  {
    'stdout' => output.first,
    'exitcode' => output.last,
  }
rescue => ex
  {
    'exception' => ex,
    'stderr' => ex.message,
    'exitcode' => 1,
  }
end