Module: RightScale::RightPopen

Defined in:
lib/right_popen.rb,
lib/right_popen/popen3_sync.rb,
lib/right_popen/process_base.rb,
lib/right_popen/target_proxy.rb,
lib/right_popen/linux/process.rb,
lib/right_popen/process_status.rb,
lib/right_popen/linux/popen3_async.rb,
lib/right_popen/safe_output_buffer.rb

Defined Under Namespace

Modules: InputHandler, PipeHandler, StatusHandler Classes: Process, ProcessBase, ProcessError, ProcessStatus, SafeOutputBuffer, TargetProxy

Constant Summary collapse

DEFAULT_POPEN3_OPTIONS =

see popen3_async for details.

{
  :directory        => nil,
  :environment      => nil,
  :exit_handler     => nil,
  :group            => nil,
  :inherit_io       => false,
  :input            => nil,
  :locale           => true,
  :pid_handler      => nil,
  :size_limit_bytes => nil,
  :stderr_handler   => nil,
  :stdout_handler   => nil,
  :target           => nil,
  :timeout_seconds  => nil,
  :umask            => nil,
  :user             => nil,
  :watch_handler    => nil,
  :watch_directory  => nil,
}

Class Method Summary collapse

Class Method Details

.popen3_async(cmd, options) ⇒ TrueClass

Spawns a process to run given command asynchronously, hooking all three standard streams of the child process. Implementation requires the eventmachine gem.

Streams the command’s stdout and stderr to the given handlers. Time- ordering of bytes sent to stdout and stderr is not preserved.

Calls given exit handler upon command process termination, passing in the resulting Process::Status.

All handlers must be methods exposed by the given target.

Parameters

Returns

Parameters:

  • options (Hash)

    for execution

Options Hash (options):

  • :directory (String)

    as initial working directory for child process or nil to inherit current working directory

  • :environment (Hash)

    variables values keyed by name

  • :exit_handler (Symbol)

    target method called on exit

  • :group (Integer|String)

    or gid for forked process (linux only)

  • :inherit_io (TrueClass|FalseClass)

    set to true to share all IO objects with forked process or false to close shared IO objects (default) (linux only)

  • :input (String)

    string that will get streamed into child’s process stdin

  • :locale (TrueClass|FalseClass)

    set to true to export LC_ALL=C in the forked environment (default) or false to use default locale (linux only)

  • :pid_handler (Symbol)

    target method called with process ID (PID)

  • :size_limit_bytes (Integer)

    for total size of watched directory after which child process will be interrupted

  • :stderr_handler (Symbol)

    target method called as error text is received

  • :stdout_handler (Symbol)

    target method called as output text is received

  • :target (Object)

    object defining handler methods to be called (no handlers can be defined if not specified)

  • :timeout_seconds (Numeric)

    after which child process will be interrupted

  • :umask (Integer|String)

    for files created by process (linux only)

  • :user (Integer|String)

    or uid for forked process (linux only)

  • :watch_handler (Symbol)

    called periodically with process during watch; return true to continue, false to abandon (sync only)

  • :watch_directory (String)

    to monitor for child process writing files

  • :async_exception_handler (Symbol)

    target method called if an exception is handled (on another thread)

Returns:

  • (TrueClass)

    always true



155
156
157
158
159
160
161
162
163
# File 'lib/right_popen.rb', line 155

def self.popen3_async(cmd, options)
  options = DEFAULT_POPEN3_OPTIONS.dup.merge(options)
  require_popen3_impl(:popen3_async)
  unless ::EM.reactor_running?
    raise ::ArgumentError, "EventMachine reactor must be running."
  end
  ::RightScale::RightPopen.popen3_async_impl(
    cmd, ::RightScale::RightPopen::TargetProxy.new(options), options)
end

.popen3_async_impl(cmd, target, options) ⇒ Object

See RightScale.popen3_async for details



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
# File 'lib/right_popen/linux/popen3_async.rb', line 131

def self.popen3_async_impl(cmd, target, options)
  # always create eventables on the main EM thread by using next_tick. this
  # prevents synchronization problems between EM threads.
  ::EM.next_tick do
    process = nil
    begin
      # create process.
      process = ::RightScale::RightPopen::Process.new(options)
      process.spawn(cmd, target)

      # connect EM eventables to open streams.
      handlers = []
      handlers << ::EM.attach(process.status_fd, ::RightScale::RightPopen::StatusHandler, process.status_fd, target)
      handlers << ::EM.attach(process.stderr, ::RightScale::RightPopen::PipeHandler, process.stderr, target, :stderr_handler)
      handlers << ::EM.attach(process.stdout, ::RightScale::RightPopen::PipeHandler, process.stdout, target, :stdout_handler)
      handlers << ::EM.attach(process.stdin, ::RightScale::RightPopen::InputHandler, process.stdin, options[:input])

      target.pid_handler(process.pid)

      # initial watch callback.
      #
      # note that we cannot abandon async watch; callback needs to interrupt
      # in this case
      target.watch_handler(process)

      # periodic watcher.
      watch_process(process, 0.1, target, handlers)
    rescue Exception => e
      # we can't raise from the main EM thread or it will stop EM.
      # the spawn method will signal the exit handler but not the
      # pid handler in this case since there is no pid. any action
      # (logging, etc.) associated with the failure will have to be
      # driven by the exit handler.
      if target
        target.async_exception_handler(e) rescue nil
        target.exit_handler(process.status) rescue nil if process
      end
    end
  end
  true
end

.popen3_sync(cmd, options) ⇒ TrueClass

Spawns a process to run given command synchronously. This is similar to the Ruby backtick but also supports streaming I/O, process watching, etc. Does not require any evented library to use.

Streams the command’s stdout and stderr to the given handlers. Time- ordering of bytes sent to stdout and stderr is not preserved.

Calls given exit handler upon command process termination, passing in the resulting Process::Status.

All handlers must be methods exposed by the given target.

Parameters

Returns

Parameters:

  • options (Hash)

    see popen3_async for details

Returns:

  • (TrueClass)

    always true



113
114
115
116
117
118
# File 'lib/right_popen.rb', line 113

def self.popen3_sync(cmd, options)
  options = DEFAULT_POPEN3_OPTIONS.dup.merge(options)
  require_popen3_impl(:popen3_sync)
  ::RightScale::RightPopen.popen3_sync_impl(
    cmd, ::RightScale::RightPopen::TargetProxy.new(options), options)
end

.popen3_sync_impl(cmd, target, options) ⇒ Object

See RightScale.popen3_sync for details



30
31
32
33
34
# File 'lib/right_popen/popen3_sync.rb', line 30

def self.popen3_sync_impl(cmd, target, options)
  process = ::RightScale::RightPopen::Process.new(options)
  process.sync_all(cmd, target)
  true
end

.require_popen3_impl(synchronicity) ⇒ TrueClass

Loads the specified implementation.

Parameters

Return

Parameters:

  • synchronicity (Symbol|String)

    to load

Returns:

  • (TrueClass)

    always true



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
# File 'lib/right_popen.rb', line 63

def self.require_popen3_impl(synchronicity)
  # implementation of Process is specific to platform.
  case RUBY_PLATFORM
  when /mswin/
    platform_subdir = 'windows'
    impl_subdir = ::File.join(platform_subdir, 'mswin')
  when /mingw/
    platform_subdir = 'windows'
    impl_subdir = ::File.join(platform_subdir, 'mingw')
  when /win32|dos|cygwin/
    raise NotImplementedError
  else
    platform_subdir = 'linux'
    impl_subdir = platform_subdir
  end
  impl_module = ::File.join(impl_subdir, 'process')

  # only require EM when async is requested.
  case synchronicity
  when :popen3_sync
    sync_module = 'popen3_sync'
  when :popen3_async
    sync_module = ::File.join(platform_subdir, 'popen3_async')
  else
    fail 'unexpected synchronicity'
  end

  # platform-specific requires.
  base_dir = ::File.join(::File.dirname(__FILE__), 'right_popen').gsub("\\", '/')
  require ::File.expand_path(impl_module, base_dir)
  require ::File.expand_path(sync_module, base_dir)
end

.watch_process(process, wait_time, target, handlers) ⇒ Object

watches process for exit or interrupt criteria. doubles the wait time up to a maximum of 1 second for next wait.

Parameters

Return

true

Always return true

Parameters:

  • process (Process)

    that was run

  • wait_time (Numeric)

    as seconds to wait before checking status

  • target (Object)

    for handler calls

  • handlers (Array)

    used by eventmachine for status, stderr, stdout, and stdin



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/right_popen/linux/popen3_async.rb', line 184

def self.watch_process(process, wait_time, target, handlers)
  ::EM::Timer.new(wait_time) do
    begin
      if process.alive?
        if process.timer_expired? || process.size_limit_exceeded?
          process.interrupt
        else
          # cannot abandon async watch; callback needs to interrupt in this case
          target.watch_handler(process)
        end
        watch_process(process, [wait_time * 2, 1].min, target, handlers)
      else
        handlers.each { |h| h.drain_and_close rescue nil }
        process.wait_for_exit_status
        target.timeout_handler rescue nil if process.timer_expired?
        target.size_limit_handler rescue nil if process.size_limit_exceeded?
        target.exit_handler(process.status) rescue nil
      end
    rescue Exception => e
      # we can't raise from the main EM thread or it will stop EM.
      # the spawn method will signal the exit handler but not the
      # pid handler in this case since there is no pid. any action
      # (logging, etc.) associated with the failure will have to be
      # driven by the exit handler.
      if target
        target.async_exception_handler(e) rescue nil
        status = process && process.status
        status ||= ::RightScale::RightPopen::ProcessStatus.new(nil, 1)
        target.exit_handler(status)
      end
    end
  end
  true
end