Class: Puppet::ResourceApi::Command

Inherits:
Object
  • Object
show all
Defined in:
lib/puppet/resource_api/command.rb

Overview

A useful interface to safely run system commands

See github.com/DavidS/puppet-specifications/blob/reasourceapi/language/resource-api/README.md#commands for a complete specification

Defined Under Namespace

Classes: Result

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command) ⇒ Command

Returns a new instance of Command.



21
22
23
24
25
# File 'lib/puppet/resource_api/command.rb', line 21

def initialize(command)
  @command = command
  @cwd = '/'
  @environment = {}
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



19
20
21
# File 'lib/puppet/resource_api/command.rb', line 19

def command
  @command
end

#cwdObject

Returns the value of attribute cwd.



17
18
19
# File 'lib/puppet/resource_api/command.rb', line 17

def cwd
  @cwd
end

#environmentObject

Returns the value of attribute environment.



17
18
19
# File 'lib/puppet/resource_api/command.rb', line 17

def environment
  @environment
end

Class Method Details

.prepare_process(_context, command, *args, environment:, cwd:) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/puppet/resource_api/command.rb', line 112

def self.prepare_process(_context, command, *args, environment:, cwd:)
  process = ChildProcess.build(command, *args)
  environment.each do |k, v|
    process.environment[k] = v
  end
  process.cwd = cwd

  process
end

Instance Method Details

#run(context, *args, stdin_source: :none, stdin_value: nil, stdin_io: nil, stdin_encoding: nil, stdout_destination: :log, stdout_loglevel: :debug, stdout_encoding: nil, stderr_destination: :log, stderr_loglevel: :warning, stderr_encoding: nil, noop: false) ⇒ Object



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
73
74
75
76
77
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
# File 'lib/puppet/resource_api/command.rb', line 27

def run(context, *args,
        stdin_source: :none, stdin_value: nil, stdin_io: nil, stdin_encoding: nil,
        stdout_destination: :log, stdout_loglevel: :debug, stdout_encoding: nil,
        stderr_destination: :log, stderr_loglevel: :warning, stderr_encoding: nil,
        noop: false)
  raise ArgumentError, "context is a '#{context.class}', expected a 'Puppet::ResourceApi::BaseContext'" unless context.is_a? Puppet::ResourceApi::BaseContext
  return if noop
  process = self.class.prepare_process(context, command, *args, environment: environment, cwd: cwd)

  process.duplex = true

  stdout_r, stdout_w = IO.pipe
  process.io.stdout = stdout_w
  process.io.stdout.set_encoding(process.io.stdout.external_encoding, stdout_encoding) if stdout_encoding

  stderr_r, stderr_w = IO.pipe
  process.io.stderr = stderr_w
  process.io.stderr.set_encoding(process.io.stderr.external_encoding, stderr_encoding) if stderr_encoding

  process.io.stdin.set_encoding(process.io.stdin.external_encoding, stdin_encoding) if stdin_encoding

  process.start
  stdout_w.close
  stderr_w.close

  case stdin_source
  when :none # rubocop:disable Lint/EmptyWhen
    # nothing to do here
  when :io
    while (v = stdin_io.read) && !v.empty? # `empty?` signals EOF, can't use the `length` variant due to encoding issues
      process.io.stdin.write v
    end
  when :value
    process.io.stdin.write stdin_value
  end
  process.io.stdin.close

  result = Result.new

  # TODO: https://tickets.puppetlabs.com/browse/PDK-542 - capture/buffer full lines
  while process.alive? || !stdout_r.eof? || !stderr_r.eof?
    rs, _ws, _errs = IO.select([stdout_r, stderr_r])
    rs.each do |pipe|
      loglevel = if pipe == stdout_r
                   stdout_loglevel
                 else
                   stderr_loglevel
                 end

      destination = if pipe == stdout_r
                      stdout_destination
                    else
                      stderr_destination
                    end

      begin
        chunk = pipe.read_nonblock(1024)
        case destination
        when :log
          chunk.split("\n").each { |l| context.send(loglevel, l.strip) }
        when :store
          if pipe == stdout_r
            result.stdout += chunk
          else
            result.stderr += chunk
          end
        end
      rescue Errno::EBADF # rubocop:disable Lint/HandleExceptions
        # This can be thrown on Windows after the process has gone away
        # ignore, retry WaitReadable through outer loop
      rescue IO::WaitReadable, EOFError # rubocop:disable Lint/HandleExceptions
        # ignore, retry WaitReadable through outer loop
      end
    end
  end

  result.exit_code = process.wait

  raise Puppet::ResourceApi::CommandExecutionError, 'Command %{command} failed with exit code %{exit_code}' % { command: command, exit_code: result.exit_code } unless result.exit_code.zero?

  result
rescue ChildProcess::LaunchError => e
  raise Puppet::ResourceApi::CommandNotFoundError, 'Error when executing %{command}: %{error}' % { command: command, error: e.to_s }
end