Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100755 301 lines (270 sloc) 7.187 kB
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
1 #!/usr/bin/env ruby
2 require File.expand_path(File.dirname(__FILE__) + '/shared')
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
3 ENV['PATH'] = OLD_PATH
4 STDOUT.sync = STDERR.sync = true
5
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
6 require 'rubygems'
7 require 'optparse'
8
9 OPTIONS = {}
10
11 def parse_options
12 parser = OptionParser.new do |opts|
13 nl = "\n" + ' ' * 37
eda38a5 @FooBarWidget Introduce run_in_pv tool
FooBarWidget authored
14 opts.banner = "Usage: ./run [options] COMMAND..."
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
15 opts.separator "Run a command with various options."
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
16 opts.separator ""
17
18 opts.separator "Options:"
6475e1f @FooBarWidget Add more help messages for 'run'
FooBarWidget authored
19 opts.on("--log-file FILE", "Log to file in addition to printing to terminal") do |value|
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
20 OPTIONS[:log_file] = value
21 end
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
22 opts.on("--append", "Append to log file instead of overwriting it.") do
23 OPTIONS[:append] = true
24 end
6475e1f @FooBarWidget Add more help messages for 'run'
FooBarWidget authored
25 opts.on("--syslog", "Log to syslog in additional to printing to terminal") do
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
26 OPTIONS[:syslog] = true
27 end
6475e1f @FooBarWidget Add more help messages for 'run'
FooBarWidget authored
28 opts.on("--pv", "Pipe output through pv") do
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
29 OPTIONS[:pv] = true
30 end
6475e1f @FooBarWidget Add more help messages for 'run'
FooBarWidget authored
31 opts.on("--program-name NAME", "Run command with the given argv[0]") do |value|
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
32 OPTIONS[:program_name] = value
33 end
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
34 opts.on("--status-file FILE") do |value|
35 OPTIONS[:status_file] = value
36 end
37 opts.on("--lock-file FILE") do |value|
38 OPTIONS[:lock_file] = value
39 end
40 opts.on("--email-to ADDRESSES", "Separated by comma") do |value|
41 OPTIONS[:email_to] = value
42 end
43 end
44 begin
45 parser.parse!
46 rescue OptionParser::ParseError => e
47 STDERR.puts e
48 STDERR.puts
49 STDERR.puts "Please see '--help' for valid options."
50 exit 1
51 end
52
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
53 if ARGV.size < 1
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
54 STDERR.puts parser
55 exit 1
56 end
57 end
58
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
59 def can_exec_directly?
60 return !OPTIONS[:log_file] && !OPTIONS[:syslog] && !OPTIONS[:pv] && !OPTIONS[:status_file] && !OPTIONS[:lock_file] && !OPTIONS[:email_to]
61 end
62
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
63 def start
64 parse_options
65
66 begin
67 lock_file = create_lock_file
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
68 create_log_file
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
69 write_status_file('')
70
f48d0ce @FooBarWidget Improve documentation for 'run'
FooBarWidget authored
71 STDIN.reopen("/dev/null", "r")
72
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
73 if has_sink?
f48d0ce @FooBarWidget Improve documentation for 'run'
FooBarWidget authored
74 main_process = spawn(main_command,
75 :out => :pipe,
76 :err => :pipe)
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
77 sink_process = spawn_sink(main_process)
78 [:in, :out].each do |channel|
79 main_process[channel].close if main_process[channel]
80 main_process.delete(channel)
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
81 end
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
82 elsif can_exec_directly?
83 exec(*main_command)
84 else
85 command = spawn(main_command)
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
86 end
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
87
88 while true
89 begin
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
90 Process.waitpid(main_process[:pid])
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
91 exit_code = ($?.exitstatus || 2)
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
92 main_process.delete(:pid)
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
93 break
94 rescue Errno::ECHILD
95 exit_code = 1
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
96 main_process.delete(:pid)
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
97 break
98 rescue SignalException => e
99 signame = get_signal_name(e)
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
100 Process.kill(signame, main_process[:pid])
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
101 end
102 end
103
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
104 # TODO: are we supposed to wait for the output sink process?
105 # If we only wait for the command then the output sink process
106 # may not have finished processing all the output yet.
107 # But if we wait for both, and the command spawns subprocesses,
108 # then the output sink process doesn't exit until all those
109 # subprocesses have also exited. Maybe we should provide a
110 # command line option for this.
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
111 if sink_process
112 sink_process[:pids].each do |pid|
113 begin
114 Process.waitpid(pid)
115 rescue Errno::ECHILD
116 # Ignore exception.
117 end
118 end
119 sink_process = nil
38f185a @FooBarWidget 'run' command should redirect output to 'tee' so that any output is i…
FooBarWidget authored
120 end
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
121
122 write_status_file(exit_code)
123 if OPTIONS[:email_to]
124 email(
125 OPTIONS[:email_from],
126 OPTIONS[:email_to],
127 "Command finished with exit code #{exit_code}: #{ARGV.join(' ')}",
128
129 "Command: #{ARGV.join(' ')}\n" +
130 "Exit code: #{exit_code}\n" +
131 "Host: #{`hostname`.strip}\n" +
132 "Log file: #{OPTIONS[:log_file]}\n"
133 )
134 end
135 exit(exit_code)
38f185a @FooBarWidget 'run' command should redirect output to 'tee' so that any output is i…
FooBarWidget authored
136
137 rescue SystemExit
138 raise
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
139
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
140 rescue Exception => e
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
141 if OPTIONS[:log_file]
142 f = File.open(OPTIONS[:log_file], 'a')
143 else
144 f = IO.popen("logger -t '#{program_name}:runner[#{$$}]'", "w")
145 end
146 begin
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
147 f.puts("#{e.class}: #{e.message || e}\n " +
148 e.backtrace.join("\n "))
beeffd9 @FooBarWidget Introduce the 'syslog-tee' tool and allow the 'run' tool to log to sy…
FooBarWidget authored
149 ensure
150 f.close
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
151 end
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
152 Process.kill('SIGTERM', main_process[:pid]) if main_process && main_process[:pid]
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
153 raise e
38f185a @FooBarWidget 'run' command should redirect output to 'tee' so that any output is i…
FooBarWidget authored
154
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
155 ensure
156 delete_lock_file(lock_file) if lock_file
157 end
158 end
159
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
160 def spawn(command, options)
161 result = {}
162 if options[:in] == :pipe
163 stdin_pipe = IO.pipe
164 result[:in] = stdin_pipe[1]
165 end
166 if options[:out] == :pipe
167 stdout_pipe = IO.pipe
168 result[:out] = stdout_pipe[0]
169 end
170 result[:pid] = fork do
171 if options[:in] == :pipe
172 STDIN.reopen(stdin_pipe[0])
173 elsif options[:in].is_a?(Array)
174 STDIN.reopen(*options[:in])
175 elsif options[:in]
176 STDIN.reopen(options[:in])
177 end
178 if options[:out] == :pipe
179 STDOUT.reopen(stdout_pipe[1])
180 elsif options[:out].is_a?(Array)
181 STDOUT.reopen(*options[:out])
182 elsif options[:out]
183 STDOUT.reopen(options[:out])
184 end
185 if options[:err] == :pipe
186 STDERR.reopen(stdout_pipe[1])
187 elsif options[:err].is_a?(Array)
188 STDERR.reopen(*options[:err])
189 elsif options[:err]
190 STDERR.reopen(options[:err])
191 end
192 stdin_pipe[1].close if stdin_pipe
193 stdout_pipe[0].close if stdout_pipe
194 if options[:setsid]
195 Process.setsid
196 end
197 begin
198 exec(*command)
199 rescue SystemCallError => e
200 STDERR.puts "Cannot execute '#{command.join(' ')}': #{e}"
201 exit! 127
202 end
203 end
204 stdin_pipe[0].close if stdin_pipe
205 stdout_pipe[1].close if stdout_pipe
206 return result
207 end
208
209 def has_sink?
210 return OPTIONS[:syslog] || OPTIONS[:log_file] || OPTIONS[:pv]
211 end
212
213 def spawn_sink(main_process)
214 if OPTIONS[:syslog]
215 command = ["#{TOOLS_DIR}/syslog-tee", "-t", "#{program_name}[#{main_process[:pid]}]"]
216 elsif OPTIONS[:log_file]
217 if OPTIONS[:append]
218 command = ["tee", "-a", OPTIONS[:log_file]]
219 else
220 command = ["tee", OPTIONS[:log_file]]
221 end
222 elsif OPTIONS[:pv]
223 command = pv_command
224 else
225 raise "Unknown options combination"
226 end
227
228 # We setsid because we don't want to let terminal signals reach any sink processes.
229 if (OPTIONS[:syslog] || OPTIONS[:log_file]) && OPTIONS[:pv]
230 # Pipeline: main_process | sink | pv
231 sink_process = spawn(command, :setsid => true, :in => main_process[:out], :out => :pipe)
232 pv_process = spawn(pv_command, :setsid => true, :in => sink_process[:out])
233 sink_process[:out].close
234 sink_process.delete(:out)
235 return { :pids => [sink_process[:pid], pv_process[:pid]] }
236 else
237 # Pipeline: main_process | sink
238 sink_process = spawn(command, :setsid => true, :in => main_process[:out])
239 return { :pids => [sink_process[:pid]] }
240 end
241 end
242
243 def main_command
244 if OPTIONS[:program_name]
245 args = ARGV.dup
246 argv0 = args.shift
247 return [[argv0, OPTIONS[:program_name]], *args]
248 else
249 return ARGV
250 end
251 end
252
253 def program_name
254 return OPTIONS[:program_name] || File.basename(ARGV[0])
255 end
256
257 def pv_command
258 return ["pv"]
259 end
260
6f39fde @FooBarWidget The 'run' tool should forward all received signals to the child process.
FooBarWidget authored
261 def get_signal_name(signal_exception)
262 if signal_exception.is_a?(Interrupt)
263 return "SIGINT"
264 else
265 return signal_exception.signm
266 end
267 end
268
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
269 def create_lock_file
270 if OPTIONS[:lock_file]
271 File.open(OPTIONS[:lock_file], File::WRONLY | File::EXCL | File::CREAT) do |f|
272 f.puts Process.pid
273 end
274 return true
275 else
276 return nil
277 end
278 rescue Errno::EEXIST
279 raise "Lock file #{OPTIONS[:lock_file]} already exists!"
280 end
281
aa105c8 @FooBarWidget Extend 'run' tool so that 'run_in_pv' is obsolete.
FooBarWidget authored
282 def create_log_file
283 if OPTIONS[:log_file]
284 File.open(OPTIONS[:log_file], OPTIONS[:append] ? 'a' : 'w').close
285 end
286 end
287
46abe63 @FooBarWidget Introduce the 'run' tool
FooBarWidget authored
288 def delete_lock_file(lock_file)
289 File.unlink(OPTIONS[:lock_file])
290 end
291
292 def write_status_file(content)
293 if OPTIONS[:status_file]
294 File.open(OPTIONS[:status_file], "w") do |f|
295 f.write(content.to_s)
296 end
297 end
298 end
299
300 start
Something went wrong with that request. Please try again.