class Roby::Tasks::ExternalProcess

This task class can be used to monitor the execution of an external process. Among the useful features, it can redirect standard output and error output to files.

The events will act as follows:

The task by default is not interruptible, because there is no good common way to gracefully terminate an external program. To e.g. use signals, one would need to explicitely make the :stop command send a signal to {#pid} and let ExternalProcess' signal handling do the rest.

Attributes

command_line[R]

This task argument is an array whose first element is the executable to start and the rest the arguments that need to be passed to it.

It can also be set to a simple string, which is interpreted as the executable name with no arguments.

pid[R]

The PID of the child process, or nil if the child process is not running

working_directory[R]

The working directory. If not set, the current directory is used.

Public Class Methods

new(arguments = Hash.new) click to toggle source
Calls superclass method Roby::Task::new
# File lib/roby/tasks/external_process.rb, line 49
def initialize(arguments = Hash.new)
    if arg = arguments[:command_line]
        arguments[:command_line] = [arg] if !arg.kind_of?(Array)
    end
    @pid = nil
    @buffer = nil
    @redirection = Hash.new
    super(arguments)
end

Public Instance Methods

create_redirection(redir_target) click to toggle source

@api privater

Handle redirection for a single stream (out or err)

# File lib/roby/tasks/external_process.rb, line 112
def create_redirection(redir_target)
    if !redir_target
        return [], nil
    elsif redir_target == :close
        return [], :close
    elsif redir_target == :pipe
        pipe, io = IO.pipe
        return [[:close, io]], io, pipe, String.new
    elsif redir_target !~ /%p/
        # Assume no replacement in redirection, just open the file
        if redir_target[0, 1] == '+'
            io = File.open(redir_target[1..-1], 'a')
        else
            io = File.open(redir_target, 'w')
        end
        return [[:close, io]], io
    else
        io = open_redirection(working_directory) 
        return [[redir_target, io]], io
    end
end
dead!(result) click to toggle source

Called to announce that this task has been killed. result is the corresponding Process::Status object.

# File lib/roby/tasks/external_process.rb, line 61
def dead!(result)
    if !result
        failed_event.emit
    elsif result.success?
        success_event.emit
    elsif result.signaled?
        signaled_event.emit(result)
    else
        failed_event.emit(result)
    end
end
handle_redirection() click to toggle source

@api private

Setup redirections pre-spawn

# File lib/roby/tasks/external_process.rb, line 137
def handle_redirection
    if !@redirection[:stdout] && !@redirection[:stderr]
        return [], Hash.new
    elsif (@redirection[:stdout] == @redirection[:stderr]) && ![:pipe, :close].include?(@redirection[:stdout])
        io = open_redirection(working_directory) 
        return [[@redirection[:stdout], io]], Hash[out: io, err: io]
    end

    out_open, out_io, @out_pipe, @out_buffer =
        create_redirection(@redirection[:stdout])
    err_open, err_io, @err_pipe, @err_buffer =
        create_redirection(@redirection[:stderr])

    if @out_buffer || @err_buffer
        @read_buffer = String.new
    end

    spawn_options = Hash.new
    spawn_options[:out] = out_io if out_io
    spawn_options[:err] = err_io if err_io
    return (out_open + err_open), spawn_options
end
kill(signo) click to toggle source

Kills the child process

@param [String,Integer] signo the signal name or number, as

accepted by Process#kill
# File lib/roby/tasks/external_process.rb, line 204
def kill(signo)
    Process.kill(signo, pid)
end
open_redirection(dir) click to toggle source

@api private

Open the output file for redirection, before spawning

# File lib/roby/tasks/external_process.rb, line 194
def open_redirection(dir)
    Dir::Tmpname.create 'roby-external-process', dir do |path, _|
        return File.open(path, 'w+')
    end
end
read_pipe(pipe, buffer) click to toggle source

@api private

Read a given pipe, when an output is redirected to pipe

# File lib/roby/tasks/external_process.rb, line 211
def read_pipe(pipe, buffer)
    received = false
    while true
        pipe.read_nonblock 1024, @read_buffer
        received = true
        buffer.concat(@read_buffer)
    end
rescue EOFError
    if received
        return true, buffer.dup
    end
rescue IO::WaitReadable
    if received
        return false, buffer.dup
    end
end
read_pipes() click to toggle source
# File lib/roby/tasks/external_process.rb, line 240
def read_pipes
    if @out_pipe
        eos, data = read_pipe(@out_pipe, @out_buffer)
        if eos
            @out_pipe = nil
        end
        if data
            stdout_received(data)
        end
    end
    if @err_pipe
        eos, data = read_pipe(@err_pipe, @err_buffer)
        if eos
            @err_pipe = nil
        end
        if data
            stderr_received(data)
        end
    end
end
redirect_output "file" click to toggle source
redirect_output stdout: "file-out", stderr: "another-file"
redirect_output nil

If set to a string, the process' standard output will be redirected to the given file. The following replacement is done:

  • '%p' is replaced by the process PID

The last form (with nil argument) removes any redirection. A specific redirection can also be disabled using the hash form:

redirect_output stdout: nil
# File lib/roby/tasks/external_process.rb, line 87
def redirect_output(common = nil, stdout: nil, stderr: nil)
    if @pid
        raise RuntimeError, "cannot change redirection after task start"
    elsif common
        stdout = stderr = common
    end

    @redirection = Hash.new
    if stdout
        @redirection[:stdout] = 
            if [:pipe, :close].include?(stdout) then stdout
            else stdout.to_str
            end
    end
    if stderr
        @redirection[:stderr] = 
            if [:pipe, :close].include?(stderr) then stderr
            else stderr.to_str
            end
    end
end
start!() click to toggle source

Starts the child process. Emits start when the process is actually started.

# File lib/roby/tasks/external_process.rb, line 165
event :start do |context|
    working_directory = (self.working_directory || Dir.pwd)
    options = Hash[pgroup: 0, chdir: working_directory]

    opened_ios, spawn_options = handle_redirection

    @pid = Process.spawn *command_line, **spawn_options
    opened_ios.each do |pattern, io|
        if pattern == :close
            io.close
        else
            FileUtils.mv io.path, File.join(working_directory, redirection_path(pattern, @pid))
        end
    end

    start_event.emit
end
stderr_received(data) click to toggle source

Method called when data is received on an intercepted stderr

Intercept stdout by calling redirect_output(stderr: :pipe)

# File lib/roby/tasks/external_process.rb, line 237
def stderr_received(data)
end
stdout_received(data) click to toggle source

Method called when data is received on an intercepted stdout

Intercept stdout by calling redirect_output(stdout: :pipe)

# File lib/roby/tasks/external_process.rb, line 231
def stdout_received(data)
end