public
Description: A process execution library which doesn't suck.
Clone URL: git://github.com/codahale/ropen.git
ropen / lib / ropen / command.rb
100644 97 lines (81 sloc) 2.166 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
require "ropen"
require "ropen/events"
require "ropen/pipe"
require "ropen/spool"
 
# TODO: document me
# TODO: add timeouts for processes stalling for lack of stdin
 
class Ropen::Command
  attr_reader :executable, :arguments, :exit_status
  
  def initialize(executable, *arguments)
    @executable = File.expand_path(executable)
    @arguments = arguments
    check_executable(executable)
    @stdout_events = Ropen::Events.new
    @stderr_events = Ropen::Events.new
    @stdin_spool = Ropen::Spool.new
    yield self if block_given?
  end
  
  def run
    initialize_streams
    pid = fork do
      redirect_streams
      exec(@executable, *@arguments)
    end
    stdin, stdout, stderr = open_streams
    process_streams(stdin, stdout, stderr, pid)
    return @exit_status
  ensure
    finalize_streams
  end
  
  def stdin
    @stdin_io || @stdin_spool
  end
  
  def stdout
    @stdout_events
  end
  
  def stderr
    @stderr_events
  end
  
private
  
  def initialize_streams
    @stdin = Ropen::Pipe.new
    @stdout = Ropen::Pipe.new
    @stderr = Ropen::Pipe.new
  end
  
  def open_streams
    @stdin.close_reader
    @stdout.close_writer
    @stderr.close_writer
    @stdin.writer.sync = true
    return [@stdin.writer, @stdout.reader, @stderr.reader]
  end
  
  def redirect_streams
    @stdin.bind_input(STDIN)
    @stdout.bind_output(STDOUT)
    @stderr.bind_output(STDERR)
  end
  
  def process_streams(stdin, stdout, stderr, child_pid)
    @stdin_io = stdin
    @stdout_events.run(stdout)
    @stderr_events.run(stderr)
    @stdin_spool.replay(stdin)
    [@stdout_events, @stderr_events].each { |e| e.finish }
    Process.waitpid(child_pid)
    @exit_status = $?
  end
  
  def finalize_streams
    [@stdin, @stdout, @stderr].each { |s| s.close }
    @stdin = nil
    @stdin_io = nil
    @stdout = nil
    @stderr = nil
  end
  
  def check_executable(executable_name)
    unless File.exist?(@executable)
      raise Ropen::InvalidExecutableError.new("#{executable_name} does not exist")
    end
    
    unless File.executable?(@executable)
      raise Ropen::InvalidExecutableError.new("#{executable_name} is not executable")
    end
  end
 
end