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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
# Run an external command, capturing its stdout and stderr
# streams into variables.
#
# So it’s rather like the `backtick` built-in, except that:
# - The command is run as-is, rather than being parsed by the shell;
# - Standard error is also captured.
#
# After the run() method has been called, the instance variables
# out, err and status contain the contents of the process’s stdout,
# the contents of its stderr, and the exit status.
#
# Example usage:
# require 'external_command'
# xc = ExternalCommand("ls", "-l").run()
# puts "Ran ls -l with exit status #{xc.status}"
# puts "===STDOUT===\n#{xc.out}"
# puts "===STDERR===\n#{xc.err}"
#
# The out and err attributes are writeable. If you assign
# a string, after calling the constructor and before calling
# run(), then the subprocess output/error will be appended
# to this string.
# <rant author="robin">
# In any sane language, this would be implemented with a
# single child process. The parent process would block on
# select(), and when the child process terminated, the
# select call would be interrupted by a CHLD signal
# and return EINTR. Unfortunately Ruby goes out of its
# way to prevent this from working, automatically restarting
# the select call if EINTR is returned. Therefore we
# use a parent-child-grandchild arrangement, where the
# parent blocks on select() and the child blocks on
# waitpid(). When the child detects that the grandchild
# has finished, it writes to a pipe that’s included in
# the parent’s select() for this purpose.
# </rant>
class ExternalCommand
attr_accessor :out, :err
attr_reader :status
def initialize(cmd, *args)
@cmd = cmd
@args = args
# Strings to collect stdout and stderr from the child process
# These may be replaced by the caller, to append to existing strings.
@out = ""
@err = ""
@fin = ""
end
def run()
# Pipes for parent-child communication
@out_read, @out_write = IO::pipe
@err_read, @err_write = IO::pipe
@fin_read, @fin_write = IO::pipe
@pid = fork do
# Here we’re in the child process.
child_process
end
# Here we’re in the parent process.
parent_process
return self
end
private
def child_process()
# Reopen stdout and stderr to point at the pipes
STDOUT.reopen(@out_write)
STDERR.reopen(@err_write)
# Close all the filehandles other than the ones we intend to use.
ObjectSpace.each_object(IO) do |fh|
fh.close unless (
[STDOUT, STDERR, @fin_write].include?(fh) || fh.closed?)
end
Process::waitpid(fork { grandchild_process })
@fin_write.puts($?.exitstatus.to_s)
exit! 0
end
def grandchild_process()
exec(@cmd, *@args)
# This is only reached if the exec fails
@err_write.print("Failed to exec: #{[@cmd, *@args].join(' ')}")
exit! 99
end
def parent_process()
# Close the writing ends of the pipes
@out_write.close
@err_write.close
@fin_write.close
@fhs = {@out_read => @out, @err_read => @err, @fin_read => @fin}
while @fin.empty?
ok = read_data
if !ok
raise "select() timed out even with a nil (infinite) timeout"
end
end
while read_data(0)
# Pull out any data that’s left in the pipes
end
Process::waitpid(@pid)
@status = @fin.to_i
@out_read.close
@err_read.close
end
def read_data(timeout=nil)
ready_array = IO.select(@fhs.keys, [], [], timeout)
return false if ready_array.nil?
ready_array[0].each do |fh|
begin
@fhs[fh] << fh.readpartial(8192)
rescue EOFError
@fhs.delete fh
end
end
return true
end
end
|