Class: Servolux::Piper

Inherits:
Object
  • Object
show all
Defined in:
lib/servolux/piper.rb

Overview

Synopsis

A Piper is used to fork a child process and then establish a communication pipe between the parent and child. This communication pipe is used to pass Ruby objects between the two.

Details

When a new piper instance is created, the Ruby process is forked into two processes - the parent and the child. Each continues execution from the point of the fork. The piper establishes a pipe for communication between the parent and the child. This communication pipe can be opened as read / write / read-write (from the perspective of the parent).

Communication over the pipe is handled by marshalling Ruby objects through the pipe. This means that nearly any Ruby object can be passed between the two processes. For example, exceptions from the child process can be marshalled back to the parent and raised there.

Object passing is handled by use of the puts and gets methods defined on the Piper. These methods use a timeout and the Kernel#select method to ensure a timely return.

Examples

piper = Servolux::Piper.new('r', :timeout => 5)

piper.parent {
  $stdout.puts "parent pid #{Process.pid}"
  $stdout.puts "child pid #{piper.pid} [from fork]"

  child_pid = piper.gets
  $stdout.puts "child pid #{child_pid} [from child]"

  msg = piper.gets
  $stdout.puts "message from child #{msg.inspect}"
}

piper.child {
  sleep 2
  piper.puts Process.pid
  sleep 3
  piper.puts "The time is #{Time.now}"
}

piper.close

Constant Summary

SIZEOF_INT =

:stopdoc:

[42].pack('I').size

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Piper) Piper.new(mode = 'r', opts = {})

Returns a new instance of Piper

Creates a new Piper instance with the communication pipe configured using the provided mode. The default mode is read-only (from the parent, and write-only from the child). The supported modes are as follows:

Mode | Parent View | Child View
-------------------------------
r      read-only     write-only
w      write-only    read-only
rw     read-write    read-write

Parameters:

  • mode (String)

    The communication mode of the pipe.

Options Hash (opts):

  • :timeout (Numeric) — default: nil

    The number of seconds to wait for a puts or gets to succeed. If not specified, calls through the pipe will block forever until data is available. You can configure the puts and gets to be non-blocking by setting the timeout to 0.



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
# File 'lib/servolux/piper.rb', line 118

def initialize( *args )
  opts = args.last.is_a?(Hash) ? args.pop : {}
  mode = args.first || 'r'

  unless %w[r w rw].include? mode
    raise ArgumentError, "Unsupported mode #{mode.inspect}"
  end

  @status = nil
  @timeout = opts.fetch(:timeout, nil)
  socket_pair = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
  @child_pid = Kernel.fork

  if child?
    @socket = socket_pair[1]
    socket_pair[0].close

    case mode
    when 'r'; @socket.close_read
    when 'w'; @socket.close_write
    end
  else
    @socket = socket_pair[0]
    socket_pair[1].close

    case mode
    when 'r'; @socket.close_write
    when 'w'; @socket.close_read
    end
  end
end

Instance Attribute Details

- (Object) socket (readonly)

The underlying socket the piper is using for communication.



96
97
98
# File 'lib/servolux/piper.rb', line 96

def socket
  @socket
end

- (Object) timeout

The timeout in seconds to wait for puts / gets commands.



93
94
95
# File 'lib/servolux/piper.rb', line 93

def timeout
  @timeout
end

Class Method Details

+ (Piper) daemon(nochdir = false, noclose = false)

Creates a new Piper with the child process configured as a daemon. The pid method of the piper returns the PID of the daemon process.

By default a daemon process will release its current working directory and the stdout/stderr/stdin file descriptors. This allows the parent process to exit cleanly. This behavior can be overridden by setting the nochdir and noclose flags to true. The first will keep the current working directory; the second will keep stdout/stderr/stdin open.

Parameters:

  • nochdir (Boolean) (defaults to: false)

    Do not change working directories

  • noclose (Boolean) (defaults to: false)

    Do not close stdin, stdout, and stderr

Returns:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/servolux/piper.rb', line 67

def self.daemon( nochdir = false, noclose = false )
  piper = self.new(:timeout => 1)
  piper.parent {
    pid = piper.gets
    raise ::Servolux::Error, 'Could not get the child PID.' if pid.nil?
    piper.instance_variable_set(:@child_pid, pid)
  }
  piper.child {
    Process.setsid                     # Become session leader.
    exit!(0) if fork                   # Zap session leader.

    Dir.chdir '/' unless nochdir       # Release old working directory.
    File.umask 0000                    # Ensure sensible umask.

    unless noclose
      STDIN.reopen  '/dev/null'        # Free file descriptors and
      STDOUT.reopen '/dev/null', 'a'   # point them somewhere sensible.
      STDERR.reopen '/dev/null', 'a'
    end

    piper.puts Process.pid
  }
  return piper
end

Instance Method Details

- (Boolean?) alive?

Returns true if the child process is alive. Returns nil if the child process has not been started.

Always returns nil when called from the child process.

Returns:

  • (Boolean, nil)


356
357
358
359
360
361
362
363
# File 'lib/servolux/piper.rb', line 356

def alive?
  return if child?
  wait(Process::WNOHANG|Process::WUNTRACED)
  Process.kill(0, @child_pid)
  true
rescue Errno::ESRCH, Errno::ENOENT
  false
end

- (Object) child {|self| ... }

Execute the block only in the child process. This method returns immediately when called from the parent process. The piper instance is passed to the block if the arity is non-zero.

Yields:

  • (self)

    Execute the block in the child process

Yield Parameters:

  • self (Piper)

    The piper instance (optional)

Returns:

  • The return value from the block or nil when called from the parent.

Raises:

  • (ArgumentError)


199
200
201
202
203
204
205
206
207
208
# File 'lib/servolux/piper.rb', line 199

def child( &block )
  return unless child?
  raise ArgumentError, "A block must be supplied" if block.nil?

  if block.arity > 0
    block.call(self)
  else
    block.call
  end
end

- (Boolean) child?

Returns true if this is the child process and false otherwise.

Returns:

  • (Boolean)


214
215
216
# File 'lib/servolux/piper.rb', line 214

def child?
  @child_pid.nil?
end

- (Piper) close

Close both the communications socket. This only affects the process from which it was called -- the parent or the child.

Returns:



155
156
157
158
# File 'lib/servolux/piper.rb', line 155

def close
  @socket.close rescue nil
  self
end

- (Boolean) closed?

Returns true if the piper has been closed. Returns false otherwise.

Returns:

  • (Boolean)


164
165
166
# File 'lib/servolux/piper.rb', line 164

def closed?
  @socket.closed?
end

- (Object) gets(default = nil)

Read an object from the communication pipe. If data is available then it is un-marshalled and returned as a Ruby object. If the pipe is closed for reading or if no data is available then the default value is returned. You can pass in the default value; otherwise it will be nil.

This method will block until the timeout is reached or data can be read from the pipe.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/servolux/piper.rb', line 263

def gets( default = nil )
  return default unless readable?

  data = @socket.read SIZEOF_INT
  return default if data.nil?

  size = data.unpack('I').first
  data = @socket.read size
  return default if data.nil?

  Marshal.load(data) rescue data
rescue SystemCallError
  return default
end

- (Object) parent {|self| ... }

Execute the block only in the parent process. This method returns immediately when called from the child process. The piper instance is passed to the block if the arity is non-zero.

Yields:

  • (self)

    Execute the block in the parent process

Yield Parameters:

  • self (Piper)

    The piper instance (optional)

Returns:

  • The return value from the block or nil when called from the child.

Raises:

  • (ArgumentError)


227
228
229
230
231
232
233
234
235
236
# File 'lib/servolux/piper.rb', line 227

def parent( &block )
  return unless parent?
  raise ArgumentError, "A block must be supplied" if block.nil?

  if block.arity > 0
    block.call(self)
  else
    block.call
  end
end

- (Boolean) parent?

Returns true if this is the parent process and false otherwise.

Returns:

  • (Boolean)


242
243
244
# File 'lib/servolux/piper.rb', line 242

def parent?
  !@child_pid.nil?
end

- (Integer?) pid

Returns the PID of the child process when called from the parent. Returns nil when called from the child.

Returns:

  • (Integer, nil)

    The PID of the child process or nil



251
252
253
# File 'lib/servolux/piper.rb', line 251

def pid
  @child_pid
end

- (Integer?) puts(obj)

Write an object to the communication pipe. Returns nil if the pipe is closed for writing or if the write buffer is full. The obj is marshalled and written to the pipe (therefore, procs and other un-marshallable Ruby objects cannot be passed through the pipe).

If the write is successful, then the number of bytes written to the pipe is returned. If this number is zero it means that the obj was unsuccessfully communicated (sorry).

Parameters:

  • obj (Object)

    The data to send to the "other" process. The object must be marshallable by Ruby (no Proc objects or lambdas).

Returns:

  • (Integer, nil)

    The number of bytes written to the pipe or nil if there was an error or the pipe is not writeable.



292
293
294
295
296
297
298
299
# File 'lib/servolux/piper.rb', line 292

def puts( obj )
  return unless writeable?

  data = Marshal.dump(obj)
  @socket.write([data.size].pack('I')) + @socket.write(data)
rescue SystemCallError
  return nil
end

- (Boolean) readable?

Returns true if the communications pipe is readable from the process and there is data waiting to be read.

Returns:

  • (Boolean)


173
174
175
176
177
# File 'lib/servolux/piper.rb', line 173

def readable?
  return false if @socket.closed?
  r,w,e = Kernel.select([@socket], nil, nil, @timeout) rescue nil
  return !(r.nil? or r.empty?)
end

- (Integer?) signal(sig)

Send the given signal to the child process. The signal may be an integer signal number or a POSIX signal name (either with or without a SIG prefix).

This method does nothing when called from the child process.

Parameters:

  • sig (String, Integer)

    The signal to send to the child process.

Returns:

  • (Integer, nil)

    The result of Process#kill or nil if called from the child process.



311
312
313
314
315
# File 'lib/servolux/piper.rb', line 311

def signal( sig )
  return if child?
  return unless alive?
  Process.kill(sig, @child_pid)
end

- (Integer?) wait(flags = 0)

Waits for the child process to exit and returns its exit status. The global variable $? is set to a Process::Status object containing information on the child process.

Always returns nil when called from the child process.

You can get more information about how the child status exited by calling the following methods on the piper instance:

* coredump?
* exited?
* signaled?
* stopped?
* success?
* exitstatus
* stopsig
* termsig

Parameters:

  • flags (Integer) (defaults to: 0)

    Bit flags that will be passed to the system level wait call. See the Ruby core documentation for Process#wait for more information on these flags.

Returns:

  • (Integer, nil)

    The exit status of the child process or nil if the child process is not running.



341
342
343
344
345
346
347
# File 'lib/servolux/piper.rb', line 341

def wait( flags = 0 )
  return if child?
  _, @status = Process.wait2(@child_pid, flags) unless @status
  exitstatus
rescue Errno::ECHILD
  nil
end

- (Boolean) writeable?

Returns true if the communications pipe is writeable from the process and the write buffer can accept more data.

Returns:

  • (Boolean)


184
185
186
187
188
# File 'lib/servolux/piper.rb', line 184

def writeable?
  return false if @socket.closed?
  r,w,e = Kernel.select(nil, [@socket], nil, @timeout) rescue nil
  return !(w.nil? or w.empty?)
end