Module: Sensu::Spawn

Defined in:
lib/sensu/spawn.rb

Constant Summary collapse

POSIX_SPAWN_PLATFORMS =
[:linux, :macosx].freeze
POSIX_SPAWN_ARCHS =
["x86_64", "i386"].freeze
@@mutex =
Mutex.new

Class Method Summary collapse

Class Method Details

.build_child_process(command) ⇒ Array

Build a child process attached to a pipe, in order to capture its output (STDERR, STDOUT). The child process will be a platform dependent shell, that is responsible for executing the provided command.

Parameters:

  • command (String)

    to run.

Returns:

  • (Array)

    child object, pipe reader, pipe writer.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/sensu/spawn.rb', line 87

def build_child_process(command)
  reader, writer = IO.pipe
  shell = case
  when on_windows?
    ["cmd", "/c"]
  else
    ["sh", "-c"]
  end
  ChildProcess.posix_spawn = posix_spawn?
  shell_command = shell + [command]
  child = ChildProcess.build(*shell_command)
  child.io.stdout = child.io.stderr = writer
  child.leader = true
  [child, reader, writer]
end

.child_process(command, options = {}) ⇒ Array

Create a child process, return its output (STDERR & STDOUT), and exit status. The child process will have its own process group, may accept data via STDIN, and have a timeout. ChildProcess Unix POSIX spawn (‘start()`) is not thread safe, so a mutex is used to allow safe execution on Ruby runtimes with real threads (JRuby).

Using stdlib’s Timeout instead of child.poll_for_exit to avoid a deadlock, when the child output is greater than the OS max buffer size.

Parameters:

  • command (String)

    to run.

  • options (Hash) (defaults to: {})

    to create a child process with.

Options Hash (options):

  • :data (String)

    to write to STDIN.

  • :timeout (Integer)

    in seconds.

Returns:

  • (Array)

    child process output and exit status.



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/sensu/spawn.rb', line 147

def child_process(command, options={})
  child, reader, writer = build_child_process(command)
  child.duplex = true if options[:data]
  @@mutex.synchronize do
    child.start
  end
  writer.close
  output = ""
  if options[:timeout]
    Timeout::timeout(options[:timeout], ChildProcess::TimeoutError) do
      output = write_and_read(child.io.stdin, reader, options[:data])
      child.wait
    end
  else
    output = write_and_read(child.io.stdin, reader, options[:data])
    child.wait
  end
  [output, child.exit_code]
rescue ChildProcess::TimeoutError
  output = "Execution timed out"
  begin
    child.stop
  rescue => error
    pid = child.pid rescue "?"
    output += " - Unable to TERM/KILL the process: ##{pid}, #{error}"
  end
  [output, 2]
rescue => error
  child.stop rescue nil
  ["Unexpected error: #{error}", 3]
end

.on_windows?TrueClass, FalseClass

Determine if the current platform is Windows.

Returns:

  • (TrueClass, FalseClass)


75
76
77
78
# File 'lib/sensu/spawn.rb', line 75

def on_windows?
  return @on_windows unless @on_windows.nil?
  @on_windows = ChildProcess.windows?
end

.posix_spawn?TrueClass, FalseClass

Determine if POSIX Spawn is used to create child processes on the current platform. ChildProcess supports POSIX Spawn for several platforms (OSs & architectures), however, Sensu only enables the use of POSIX Spawn on a select few.

Returns:

  • (TrueClass, FalseClass)


65
66
67
68
69
70
# File 'lib/sensu/spawn.rb', line 65

def posix_spawn?
  return @posix_spawn unless @posix_spawn.nil?
  platform_supported = POSIX_SPAWN_PLATFORMS.include?(ChildProcess.os)
  arch_supported = POSIX_SPAWN_ARCHS.include?(ChildProcess.arch)
  @posix_spawn = platform_supported && arch_supported
end

.process(command, options = {}, &callback) ⇒ Object

Spawn a child process. The EventMachine reactor (loop) must be running for this method to work.

Parameters:

  • command (String)

    to run.

  • options (Hash) (defaults to: {})

    to create a child process with.

  • callback (Proc)

    called when the child process exits, its output and exit status are passed as parameters.

Options Hash (options):

  • :data (String)

    to write to STDIN.

  • :timeout (Integer)

    in seconds.



51
52
53
54
55
56
57
# File 'lib/sensu/spawn.rb', line 51

def process(command, options={}, &callback)
  create = Proc.new do
    child_process(command, options)
  end
  setup(options) unless @process_worker
  @process_worker.enqueue(create, callback)
end

.setup(options = {}) ⇒ Object

Setup a spawn process worker, to limit the number of concurrent child processes allowed at one time. This method creates the spawn process worker instance variable: ‘@process_worker`.

Parameters:

  • options (Hash) (defaults to: {})

    to create a process worker with.

Options Hash (options):

  • :limit (Integer)

    max number of child processes at a time.



37
38
39
40
# File 'lib/sensu/spawn.rb', line 37

def setup(options={})
  limit = options[:limit] || 12
  @process_worker ||= EM::Worker.new(:concurrency => limit)
end

.write_and_read(writer, reader, data) ⇒ String

Write data to a stream/file and read a stream/file until end of file (EOF).

Parameters:

  • writer (Object)

    to write data to (optional).

  • reader (Object)

    to read contents of until EOF.

  • data (String)

    to be written to writer.

Returns:

  • (String)

    the reader stream/file contents.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/sensu/spawn.rb', line 110

def write_and_read(writer, reader, data)
  buffer = (data || "").dup
  output = ""
  loop do
    unless buffer.empty?
      writer.write(buffer.slice!(0, 8191))
      writer.close if buffer.empty?
    end
    begin
      readable, _ = IO.select([reader], nil, nil, 0)
      if readable || buffer.empty?
        output << reader.readpartial(8192)
      end
    rescue EOFError
      reader.close
      break
    end
  end
  output
end