@@ -45,14 +45,17 @@ module CommandRunner
45
45
# Fx. redirecting stderr to /dev/null would look like:
46
46
# run('ls', 'nosuchfile', options: {:err => "/dev/null"})
47
47
#
48
+ # For simple case of splitting stderr into a buffer separate from stdout you can pass
49
+ # the argument split_stderr: true. This will make an :err entry available in the result.
50
+ #
48
51
# All Kernel.spawn features, like setting umasks, process group, and are supported through the options hash.
49
52
#
50
53
# Debugging: To help debugging your app you can set the debug_log parameter. It can be any old object responding
51
54
# to :puts. Fx. $stderr, $stdout, or the write end of an IO.pipe. CommandRunnerNG will put some info about
52
55
# all process start, stop, and timeouts here. To enable debug logging for all commands call
53
56
# CommandRunner.set_debug_log!($stderr) (or with some other object responding to :puts).
54
57
#
55
- def self . run ( *args , timeout : nil , environment : { } , debug_log : nil , options : DEFAULT_OPTIONS )
58
+ def self . run ( *args , timeout : nil , environment : { } , debug_log : nil , split_stderr : false , options : DEFAULT_OPTIONS )
56
59
if debug_log . nil?
57
60
debug_log = @@global_debug_log
58
61
end
@@ -88,11 +91,19 @@ def self.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFA
88
91
deadline_sequence = [ { :deadline => MAX_TIME , :action => 0 } ]
89
92
end
90
93
94
+ if split_stderr
95
+ err_r , err_w = IO . pipe
96
+ errbuf = ""
97
+ options = options . merge ( { :err => err_w } )
98
+ end
99
+
91
100
# Spawn child, merging stderr into stdout
92
101
io = IO . popen ( environment , *args , options )
93
102
debug_log . puts ( "CommandRunnerNG spawn: args=#{ args } , timeout=#{ timeout } , options: #{ options } , PID: #{ io . pid } " ) if debug_log . respond_to? ( :puts )
94
103
data = ""
95
104
105
+ err_w . close if split_stderr
106
+
96
107
# Run through all deadlines until command completes.
97
108
# We could merge this block into the selecting block above,
98
109
# but splitting like this saves us a Process.wait syscall per iteration.
@@ -101,12 +112,18 @@ def self.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFA
101
112
while Time . now < point [ :deadline ]
102
113
if Process . wait ( io . pid , Process ::WNOHANG )
103
114
read_nonblock_safe! ( io , data , tick )
115
+ read_nonblock_safe! ( err_r , errbuf , 0 ) if split_stderr
104
116
result = { :out => data , :status => $?, pid : io . pid }
117
+ if split_stderr
118
+ result [ :err ] = errbuf
119
+ err_r . close
120
+ end
105
121
debug_log . puts ( "CommandRunnerNG exit: PID: #{ io . pid } , code: #{ result [ :status ] . exitstatus } " ) if debug_log . respond_to? ( :puts )
106
122
io . close
107
123
return result
108
124
elsif !eof
109
125
eof = read_nonblock_safe! ( io , data , tick )
126
+ read_nonblock_safe! ( err_r , errbuf , 0 ) if split_stderr
110
127
end
111
128
end
112
129
@@ -136,9 +153,19 @@ def self.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFA
136
153
end
137
154
138
155
# Either we didn't have a deadline, or none of the deadlines killed off the child.
156
+ loop do
157
+ dead = read_nonblock_safe! ( io , data , tick )
158
+ read_nonblock_safe! ( err_r , errbuf , 0 ) if split_stderr
159
+ break if dead
160
+ end
139
161
Process . wait ( io . pid )
140
- read_nonblock_safe! ( io , data , tick )
162
+
141
163
result = { :out => data , :status => $?, pid : io . pid }
164
+ if split_stderr
165
+ result [ :err ] = errbuf
166
+ err_r . close
167
+ end
168
+
142
169
debug_log . puts ( "CommandRunnerNG exit: PID: #{ io . pid } , code: #{ result [ :status ] . exitstatus } " ) if debug_log . respond_to? ( :puts )
143
170
144
171
io . close
@@ -157,8 +184,8 @@ def self.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFA
157
184
# git.run(:pull, 'origin', 'master')
158
185
# git.run(:pull, 'origin', 'master', timeout: 2) # override default timeout of 10
159
186
# git.run(:status) # will raise an error because :status is not in list of allowed commands
160
- def self . create ( *args , timeout : nil , environment : { } , allowed_sub_commands : [ ] , debug_log : nil , options : DEFAULT_OPTIONS )
161
- CommandInstance . new ( args , timeout , environment , allowed_sub_commands , debug_log , options )
187
+ def self . create ( *args , timeout : nil , environment : { } , allowed_sub_commands : [ ] , debug_log : nil , split_stderr : false , options : DEFAULT_OPTIONS )
188
+ CommandInstance . new ( args , timeout , environment , allowed_sub_commands , debug_log , split_stderr , options )
162
189
end
163
190
164
191
# Log all command line invocations to a logger object responding to :puts. Set to nil to disable.
@@ -178,7 +205,7 @@ def self.set_debug_log!(logger)
178
205
179
206
class CommandInstance
180
207
181
- def initialize ( default_args , default_timeout , default_environment , allowed_sub_commands , debug_log , options )
208
+ def initialize ( default_args , default_timeout , default_environment , allowed_sub_commands , debug_log , split_stderr , options )
182
209
unless default_args . first . is_a? Array
183
210
raise "First argument must be an array of command line args. Found #{ default_args } "
184
211
end
@@ -188,6 +215,7 @@ def initialize(default_args, default_timeout, default_environment, allowed_sub_c
188
215
@default_environment = default_environment
189
216
@allowed_sub_commands = allowed_sub_commands
190
217
@debug_log = debug_log
218
+ @split_stderr = split_stderr
191
219
@options = options
192
220
end
193
221
@@ -214,6 +242,7 @@ def run(*args, timeout: nil, environment: {})
214
242
timeout : ( timeout || @default_timeout ) ,
215
243
environment : @default_environment . merge ( environment ) ,
216
244
debug_log : @debug_log ,
245
+ split_stderr : @split_stderr ,
217
246
options : @options )
218
247
end
219
248
0 commit comments