Class: HybridPlatformsConductor::CmdRunner

Inherits:
Object
  • Object
show all
Includes:
LoggerHelpers
Defined in:
lib/hybrid_platforms_conductor/cmd_runner.rb

Overview

API to execute local commands, with IO control over files, stdout, timeouts, exceptions.

Defined Under Namespace

Classes: TimeoutError, UnexpectedExitCodeError

Constant Summary

Constants included from LoggerHelpers

LoggerHelpers::LEVELS_MODIFIERS, LoggerHelpers::LEVELS_TO_STDERR

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from LoggerHelpers

#err, #init_loggers, #log_component=, #log_debug?, #log_level=, #out, #section, #set_loggers_format, #stderr_device, #stderr_device=, #stderr_displayed?, #stdout_device, #stdout_device=, #stdout_displayed?, #stdouts_to_s, #with_progress_bar

Constructor Details

#initialize(logger: Logger.new($stdout), logger_stderr: Logger.new($stderr)) ⇒ CmdRunner

Constructor

Parameters
  • logger (Logger): Logger to be used [default = Logger.new(STDOUT)]

  • logger_stderr (Logger): Logger to be used for stderr [default = Logger.new(STDERR)]



40
41
42
43
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 40

def initialize(logger: Logger.new($stdout), logger_stderr: Logger.new($stderr))
  init_loggers(logger, logger_stderr)
  @dry_run = false
end

Instance Attribute Details

#dry_runObject

Dry-run switch. When true, then commands are just printed out without being executed.

Boolean


33
34
35
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 33

def dry_run
  @dry_run
end

Class Method Details

.executables_prefixObject

Return the executables prefix to use to execute commands

Result
  • String: The executable prefix



27
28
29
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 27

def self.executables_prefix
  $PROGRAM_NAME.include?('/') ? "#{File.dirname($PROGRAM_NAME)}/" : ''
end

Instance Method Details

#options_parse(options_parser) ⇒ Object

Complete an option parser with options meant to control this Actions Executor

Parameters
  • options_parser (OptionParser): The option parser to complete



49
50
51
52
53
54
55
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 49

def options_parse(options_parser)
  options_parser.separator ''
  options_parser.separator 'Command runner options:'
  options_parser.on('-s', '--show-commands', 'Display the commands that would be run instead of running them') do
    @dry_run = true
  end
end

#root?Boolean

Is the current user root? Look into the environment to decide.

Result

Boolean: Is the current user root?



184
185
186
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 184

def root?
  whoami == 'root'
end

#run_cmd(cmd, log_to_file: nil, log_to_stdout: true, log_stdout_to_io: nil, log_stderr_to_io: nil, expected_code: 0, timeout: nil, no_exception: false, force_bash: false) ⇒ Object

Run an external command. Handle dry-run mode, timeout, and check for an expected return code. Raise an exception if the exit status is not the expected one.

Parameters
  • cmd (String or SecretString): Command to be run

  • log_to_file (String or nil): Log file capturing stdout or stderr (or nil for none). [default: nil]

  • log_to_stdout (Boolean): Do we send the output to stdout? [default: true]

  • log_stdout_to_io (IO or nil): IO to send command’s stdout to, or nil for none. [default: nil]

  • log_stderr_to_io (IO or nil): IO to send command’s stderr to, or nil for none. [default: nil]

  • expected_code (Integer, Symbol or Array<Integer or Symbol>): Return codes (or single one) that is expected [default: 0] Symbol error codes can be used:

    • command_error: The command could not be executed

    • timeout: The command ended in timeout

  • timeout (Integer or nil): Timeout to apply for the command to be run, or nil for no timeout [default: nil]

  • no_exception (Boolean): If true, don’t throw exception in case of error [default: false]

  • force_bash (Boolean): If true, then make sure command is invoked with bash instead of sh [default: false]

Result
  • Integer or Symbol: Exit status of the command, or Symbol in case of error. In case of dry-run mode the expected code is returned without executing anything.

  • String: Standard output of the command

  • String: Standard error output of the command (can be a descriptive message of the error in case of error)



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 78

def run_cmd(
  cmd,
  log_to_file: nil,
  log_to_stdout: true,
  log_stdout_to_io: nil,
  log_stderr_to_io: nil,
  expected_code: 0,
  timeout: nil,
  no_exception: false,
  force_bash: false
)
  expected_code = [expected_code] unless expected_code.is_a?(Array)
  if @dry_run
    out cmd
    [expected_code.first, '', '']
  else
    log_debug "#{timeout.nil? ? '' : "[ Timeout #{timeout} ] - "}#{cmd.light_cyan.bold}"
    exit_status = nil
    cmd_stdout = nil
    cmd_stderr = nil
    file_output =
      if log_to_file
        if File.exist?(log_to_file)
          File.open(log_to_file, 'a')
        else
          FileUtils.mkdir_p(File.dirname(log_to_file))
          File.open(log_to_file, 'w')
        end
      end
    start_time = Time.now if log_debug?
    bash_file = nil
    if force_bash
      bash_file = Tempfile.new('hpc_bash')
      bash_file.write(cmd.to_unprotected)
      bash_file.chmod 0o700
      bash_file.close
      cmd = "/bin/bash -c #{bash_file.path}"
    end
    begin
      # Make sure we keep a trace of stdout and stderr, even if it was not asked, just to use it in case of exceptions raised
      cmd_result_stdout = ''
      cmd_result_stderr = ''
      # Route IOs
      stdout_queue = Queue.new
      stderr_queue = Queue.new
      IoRouter.with_io_router(
        stdout_queue => [cmd_result_stdout] +
          (log_stdout_to_io ? [log_stdout_to_io] : []) +
          (log_to_stdout ? [@logger] : []) +
          (file_output.nil? ? [] : [file_output]),
        stderr_queue => [cmd_result_stderr] +
          (log_stderr_to_io ? [log_stderr_to_io] : []) +
          (log_to_stdout ? [@logger_stderr] : []) +
          (file_output.nil? ? [] : [file_output])
      ) do
        Bundler.without_bundled_env do
          cmd_result = TTY::Command.new(
            printer: :null,
            pty: true,
            timeout: timeout,
            uuid: false
          ).run!(cmd.to_unprotected) do |stdout, stderr|
            stdout_queue << stdout if stdout
            stderr_queue << stderr if stderr
          end
          exit_status = cmd_result.exit_status
          cmd_stdout = cmd_result.out
          cmd_stderr = cmd_result.err
        end
      end
    rescue TTY::Command::TimeoutExceeded
      exit_status = :timeout
      cmd_stdout = cmd_result_stdout
      cmd_stderr = "#{cmd_result_stderr.empty? ? '' : "#{cmd_result_stderr}\n"}Timeout of #{timeout} triggered"
    rescue
      exit_status = :command_error
      cmd_stdout = cmd_result_stdout
      cmd_stderr = "#{cmd_result_stderr.empty? ? '' : "#{cmd_result_stderr}\n"}#{$ERROR_INFO}\n#{$ERROR_INFO.backtrace.join("\n")}"
    ensure
      file_output&.close
      bash_file&.unlink
    end
    if log_debug?
      elapsed = Time.now - start_time
      log_debug "Finished in #{elapsed} seconds with exit status #{exit_status} (#{(expected_code.include?(exit_status) ? 'success'.light_green : 'failure'.light_red).bold})"
    end
    unless expected_code.include?(exit_status)
      error_title = "Command '#{cmd.to_s.split("\n").first}' returned error code #{exit_status} (expected #{expected_code.join(', ')})."
      if no_exception
        # We consider the caller is responsible for logging what he wants about the details of the error (stdout and stderr)
        log_error error_title
      else
        # The exception won't contain stdout and stderr details (unless output to stdout was on), so dump them now
        log_error "#{error_title}#{log_to_stdout ? '' : "\n----- Command STDOUT:\n#{cmd_stdout}\n----- Command STDERR:\n#{cmd_stderr}"}"
        raise exit_status == :timeout ? TimeoutError : UnexpectedExitCodeError, error_title
      end
    end
    [exit_status, cmd_stdout, cmd_stderr]
  end
end

#whoamiObject

Who is the local user? Keep a cache of it.

Result

String: Name of the local user



193
194
195
196
197
198
199
# File 'lib/hybrid_platforms_conductor/cmd_runner.rb', line 193

def whoami
  unless defined?(@whoami)
    _exit_status, stdout, _stderr = run_cmd 'whoami', log_to_stdout: log_debug?
    @whoami = stdout.strip
  end
  @whoami
end