tellmewhen 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README.textile +50 -0
  2. data/Rakefile +10 -0
  3. data/bin/tellmewhen +383 -0
  4. data/tellmewhen.gemspec +28 -0
  5. metadata +79 -0
@@ -0,0 +1,50 @@
1
+ h1. tellmewhen
2
+
3
+ Tell me when another program has finshed. Show me when it started, when it finished and how long it took. Show me the output and if it errored.
4
+
5
+ I created this utility because I had long running database scripts that I wanted a completion notification for - including a summary of the time and exit code. I had done this various ways in the past by using bash and other shell utilities. This program captures all of the features and behavior I wanted without having to script it from scratch again every time I needed it.
6
+
7
+ * YML Based Configuration in an rc file
8
+ * easy default configuration
9
+ * sends email
10
+ * includes start/stop/elapsed timing
11
+ * success/fail based on exit code
12
+ * watch an already running process (by pid, by grep pattern over 'ps aux')
13
+ * release as a gem so it's easy to use/install
14
+
15
+ h1. Configuration
16
+
17
+ Configuration Defaults:
18
+
19
+ Configuration is then merged from @$HOME/.tellmewhenrc@ if the file exists in @$HOME@. Configuration is also merged from @./.tellmewhenrc@ in the current directory, if the file exists. This allows you some flexibility in overriding settings.
20
+
21
+ h1. Usage
22
+
23
+ pre. Usage: ./tellmewhen command args...
24
+ -v, --[no-]verbose Run Verbosely.
25
+ -c, --config=file Use alternate configuration file.
26
+ -p, --pid=pid Wait for <pid> to terminate.
27
+ -e, --exists=file Wait for <file> to exist.
28
+ -m, --modified=file Wait for <file> to be modified.
29
+ -t, --timeout=seconds Wait for up to <seconds> seconds for the command before sendin a 'pending' notification.
30
+ -w, --write-config=file Write [fully merged] configuration to <file> (NB: will not be clobber).
31
+
32
+
33
+ h1. Examples
34
+
35
+ pre. ./tellmewhen 'sleep 3; ls'
36
+ ./tellmewhen -p 12345
37
+ ./tellmewhen -e some-file.txt # await existance
38
+ ./tellmewhen -m some-file.txt # await update
39
+
40
+ h1. Authors
41
+
42
+ h1. License
43
+
44
+ h1. Patches Welcome
45
+
46
+ I'd like to clean up the internals: how emails are composed.
47
+
48
+ I'd like to support other notification channels, like Instant Messaging or SMS. Multiple at one time, controlled through the configuration yaml.
49
+
50
+ What would you like?
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/gempackagetask'
4
+ require 'spec/rake/spectask'
5
+ load 'tellmewhen.gemspec'
6
+
7
+
8
+ Rake::GemPackageTask.new(SPEC) do |t|
9
+ t.need_tar = true
10
+ end
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'yaml'
5
+ require 'optparse'
6
+ require 'tempfile'
7
+ require 'net/smtp'
8
+
9
+ class TellMeWhen
10
+ RC_FILE = "#{ENV['HOME']}/.tellmewhenrc"
11
+ LOCAL_FILE = ".tellmewhenrc"
12
+
13
+ def initialize
14
+ @stdout_file = Tempfile.new('tellmewhen.stdout').path
15
+ @stderr_file = Tempfile.new('tellmewhen.stdout').path
16
+ end
17
+
18
+ def hostname
19
+ `hostname`.chomp
20
+ end
21
+
22
+ def load_settings
23
+ @settings ||= {
24
+ 'notify-via' => 'email',
25
+ 'email' => {
26
+ 'to' => "#{ENV['LOGNAME']}@#{hostname}",
27
+ 'from' => "#{ENV['LOGNAME']}@#{hostname}",
28
+ 'smtp_host' => 'localhost',
29
+ 'smtp_port' => '25'
30
+ }
31
+ }
32
+
33
+ if File.exist? RC_FILE
34
+ settings = YAML.load_file RC_FILE
35
+ @settings = @settings.merge(settings)
36
+ puts "Loaded Settings [#{RC_FILE}]: #{@settings.inspect}"
37
+ end
38
+
39
+ if File.exist? LOCAL_FILE
40
+ settings = YAML.load_file LOCAL_FILE
41
+ @settings = @settings.merge(settings)
42
+ puts "Loaded Settings [#{LOCAL_FILE}]: #{@settings.inspect}"
43
+ end
44
+ end
45
+
46
+ def save_settings target_file=RC_FILE
47
+ if ! File.exist? target_file
48
+ File.open(target_file,"w") do |f|
49
+ f.write @settings.to_yaml
50
+ end
51
+ end
52
+ end
53
+
54
+ def parse_options
55
+ @options = {
56
+ :wait_on => :command,
57
+ :wait_timeout => 600 # every 10 min send a 'pending' email
58
+ }
59
+ OptionParser.new do |opts|
60
+ opts.banner = "Usage: #$0 command args..."
61
+
62
+ opts.on("-v","--[no-]verbose", "Run Verbosely.") do |v|
63
+ @options[:verbose] = v
64
+ end
65
+
66
+ opts.on("-c","--config=file", "Use alternate configuration file.") do |file|
67
+ @options[:config_file] = file
68
+ end
69
+
70
+ opts.on("-p","--pid=pid", "Wait for <pid> to terminate.") do |pid|
71
+ @options[:wait_on] = :pid
72
+ @options[:pid] = pid
73
+ end
74
+
75
+ opts.on("-e","--exists=file", "Wait for <file> to exist.") do |file|
76
+ @options[:wait_on] = :file_exists
77
+ @options[:trigger_file] = file
78
+ end
79
+
80
+ opts.on("-m","--modified=file", "Wait for <file> to be modified.") do |file|
81
+ @options[:wait_on] = :file_modified
82
+ @options[:trigger_file] = file
83
+ end
84
+
85
+ opts.on("-t","--timeout=seconds", "Wait for up to <seconds> seconds for the command before sendin a 'pending' notification.") do |seconds|
86
+ @options[:wait_timeout] = seconds
87
+ end
88
+
89
+ opts.on("-w","--write-config=file", "Write [fully merged] configuration to <file> (NB: will not be clobber).") do |file|
90
+ @options[:write_config_to] = file
91
+ end
92
+
93
+ end.parse!
94
+
95
+ puts "Options: #{@options.inspect}"
96
+ end
97
+
98
+ def self.main args
99
+ app = self.new
100
+ app.parse_options
101
+ app.load_settings
102
+ exit app.run args
103
+ end
104
+
105
+ def elapsed_time
106
+ Time.now.to_i - @start_time.to_i
107
+ end
108
+
109
+ def wait_timeout
110
+ @options[:wait_timeout]
111
+ end
112
+
113
+ def wait_on_command args
114
+ puts "Do run: #{args}"
115
+ if args.to_s.empty?
116
+ raise "Error: you must supply a command to execute"
117
+ end
118
+ child_pid = Kernel.fork
119
+ if child_pid.nil?
120
+ # in child
121
+ STDOUT.reopen(File.open(@stdout_file, 'w+'))
122
+ STDERR.reopen(File.open(@stderr_file, 'w+'))
123
+ STDIN.close
124
+ exec "bash", "-c", args.to_s
125
+ else
126
+ # in parent
127
+ child_exited = false
128
+ while ! child_exited
129
+ if Process.wait child_pid, Process::WNOHANG
130
+ puts "Child exited: #{$?.exitstatus}"
131
+ @exit_status = $?.exitstatus
132
+ child_exited = true
133
+ end
134
+ sleep(0.250)
135
+ if elapsed_time > wait_timeout
136
+ puts "Exceeded timeout #{wait_timeout}, sending 'pending' notificaiton"
137
+ send_pending_notification
138
+ end
139
+ end
140
+ end
141
+
142
+ @end_time = Time.now
143
+ if @exit_status == 0
144
+ body = <<-BODY
145
+ Just wanted to let you know that:
146
+
147
+ #{args.to_s}
148
+
149
+ completed on #{hostname}, with an exit code of: #{@exit_status}
150
+
151
+ It started at #{@start_time.to_s} (#{@start_time.to_i}), finished at #{@end_time.to_s} (#{@end_time.to_i}) and took a total of #{elapsed_time} seconds.
152
+
153
+ May your day continue to be full of win.
154
+
155
+ Sincerely,
156
+
157
+ Tellmewhen
158
+
159
+ #{email_footer}
160
+ BODY
161
+
162
+ send_email_notification "When! SUCCESS for #{args.to_s.split.first}...", body
163
+ else
164
+ body = <<-BODY
165
+ Just wanted to let you know that:
166
+
167
+ #{args.to_s}
168
+
169
+ FAILED! on #{hostname}, with an exit code of: #{@exit_status}
170
+
171
+ It started at #{@start_time.to_s} (#{@start_time.to_i}), finished at #{@end_time.to_s} (#{@end_time.to_i}) and took a total of #{elapsed_time} seconds to collapse in a steaming heap of failure.
172
+
173
+ Have a nice day.
174
+
175
+ Warmest Regards,
176
+
177
+ Tellmewhen
178
+
179
+ #{email_footer}
180
+ BODY
181
+
182
+ send_email_notification "When! FAILURE for #{args.split.first}...", body
183
+ end
184
+ end
185
+
186
+ def send_pending_notification
187
+ body = <<-BODY
188
+ Just wanted to let you know that:
189
+
190
+ #{args.to_s}
191
+
192
+ Is _STILL_ running on #{hostname}, it has not exited. You did want me to let you know didn't you?
193
+
194
+ I started the darn thing at #{@start_time.to_s} (#{@start_time.to_i}) and it has taken a total of #{elapsed_time} seconds so far.
195
+
196
+ Just thought you'd like to know. I'll continue to keep watching what you asked me to. (boor-ring!)
197
+
198
+ Cordially,
199
+
200
+ Tellmewhen
201
+
202
+ #{email_footer}
203
+ BODY
204
+
205
+ send_email_notification "When! [NOT] I'm _still_ waiting for #{args.split.first}...", body
206
+ end
207
+
208
+ def email_footer
209
+ return <<-END
210
+ P.S. stderr says:
211
+ --
212
+ #{File.read(@stderr_file)}
213
+ --
214
+
215
+ P.S. stdout says:
216
+ --
217
+ #{File.read(@stdout_file)}
218
+ --
219
+ END
220
+ end
221
+
222
+ def pid_running? pid
223
+ lines = `ps #{pid}`.split "\n"
224
+ lines.count > 1
225
+ end
226
+
227
+ def wait_on_pid args
228
+ # wait until pid exits
229
+ while pid_running? @options[:pid]
230
+ sleep 0.250
231
+ if elapsed_time > wait_timeout
232
+ puts "Exceeded timeout #{wait_timeout}, sending 'pending' notificaiton"
233
+ send_email_notification "When! [NOT] still awaiting pid:#{pid} to exit", <<-END
234
+
235
+ I started watching #{pid} on #{hostname} at #{@start_time.to_s} (#{@start_time.to_i}), I've been watching 'em for #{elapsed_time} seconds so far.
236
+
237
+ Awaiting it's demise,
238
+
239
+ TellMeWhen
240
+ END
241
+ end
242
+ end
243
+
244
+ @end_time = Time.now
245
+
246
+ send_email_notification "When! pid:#{@options[:pid]} has come to its end", <<-END
247
+
248
+ I started watching #{@options[:pid]} on #{hostname} at #{@start_time.to_s} (#{@start_time.to_i}), and now, after #{elapsed_time} seconds it has finally gone bellly up. #{@options[:pid]} will rest in peace as of #{@end_time.to_s} (#{@end_time.to_i})
249
+
250
+ #{@options[:pid]} will be missed, it was a good little process. :,)
251
+
252
+ TellMeWhen
253
+ END
254
+
255
+ end
256
+
257
+ def wait_on_file_exists args
258
+ while !File.exist? @options[:trigger_file]
259
+ sleep 0.250
260
+ if elapsed_time > wait_timeout
261
+ puts "Exceeded timeout #{wait_timeout}, sending 'pending' notificaiton"
262
+ send_email_notification "When! [NOT] still awaiting #{@options[:trigger_file]} to exist", <<-END
263
+
264
+ I started watching for #{@options[:trigger_file]} on #{hostname} at #{@start_time.to_s} (#{@start_time.to_i}), I've been watching for it #{elapsed_time} seconds so far.
265
+
266
+ Awaiting it's arrival,
267
+
268
+ TellMeWhen
269
+ END
270
+ end
271
+ end
272
+ @end_time = Time.now
273
+
274
+ send_email_notification "When! #{@options[:trigger_file]} now exists.", <<-END
275
+
276
+ I started watching for #{@options[:trigger_file]} on #{hostname} at #{@start_time.to_s} (#{@start_time.to_i}), and now, after #{elapsed_time} seconds it has finally shown up as of #{@end_time.to_s} (#{@end_time.to_i})
277
+
278
+ What is thy next bidding my master?
279
+
280
+ TellMeWhen
281
+ END
282
+ end
283
+
284
+ def wait_on_file_modified args
285
+ trigger_file = @options[:trigger_file]
286
+ initial_mtime = File.mtime trigger_file
287
+
288
+ while initial_mtime == File.mtime(trigger_file)
289
+ sleep 0.250
290
+ if elapsed_time > wait_timeout
291
+ puts "Exceeded timeout #{wait_timeout}, sending 'pending' notificaiton"
292
+ send_email_notification "When! [NOT] still awaiting #{@options[:trigger_file]} to change", <<-END
293
+
294
+ I started watching for #{@options[:trigger_file]} to be updated on #{hostname} at #{@start_time.to_s} (#{@start_time.to_i}), I've been watching for it #{elapsed_time} seconds so far.
295
+
296
+ Awaiting it's update,
297
+
298
+ TellMeWhen
299
+ END
300
+ end
301
+ end
302
+
303
+ @end_time = Time.now
304
+
305
+ send_email_notification "When! #{@options[:trigger_file]} was updated.", <<-END
306
+
307
+ I started watching for #{@options[:trigger_file]} to be updated on #{hostname} at #{@start_time.to_s} (#{@start_time.to_i}), and now, after #{elapsed_time} seconds it has finally been modified as of #{@end_time.to_s} (#{@end_time.to_i})
308
+
309
+ POSIX is my zen,
310
+
311
+ TellMeWhen
312
+ END
313
+ end
314
+
315
+ def smtp_host
316
+ @settings["email"]["smtp_host"]
317
+ end
318
+
319
+ def smtp_port
320
+ @settings["email"]["smtp_port"]
321
+ end
322
+
323
+ def send_email_notification subject, body
324
+ # optionally send via /usr/bin/mail or sendmail binary if it exists...
325
+ puts "Sending email: from:#{@settings["email"]["from"]} to:#{@settings["email"]["to"]}"
326
+ begin
327
+ Net::SMTP.start(smtp_host, smtp_port) do |smtp|
328
+ smtp.open_message_stream('from_addr', @settings["email"]["to"].split(',')) do |f|
329
+ f.puts "From: #{@settings["email"]["from"]}"
330
+ f.puts "To: #{@settings["email"]["to"]}"
331
+ f.puts "Subject: #{subject}"
332
+ f.puts ""
333
+ f.puts body
334
+ end
335
+ end
336
+ end
337
+ rescue Errno::ECONNREFUSED => e
338
+ if File.exist? "/usr/sbin/sendmail"
339
+ body_file = Tempfile.new("tellmewhen.mail.body")
340
+ File.open(body_file.path,"w") do |f|
341
+ f.puts "From: #{@settings["email"]["from"]}"
342
+ f.puts "To: #{@settings["email"]["to"]}"
343
+ f.puts "Subject: #{subject}"
344
+ f.puts ""
345
+ f.write body
346
+ end
347
+ system "/usr/sbin/sendmail -f #{@settings["email"]["from"]} '#{@settings["email"]["to"]}' < #{body_file.path}"
348
+ elsif File.exist? "/usr/bin/mail"
349
+ raise "Implement sending via /usr/bin/mail"
350
+ body_file = Tempfile.new("tellmewhen.mail.body")
351
+ File.open(body_file.path,"w") do |f|
352
+ f.puts "From: #{@settings["email"]["from"]}"
353
+ f.puts "To: #{@settings["email"]["to"]}"
354
+ f.puts "Subject: #{subject}"
355
+ f.puts ""
356
+ f.write body
357
+ end
358
+ system "/usr/bin/mail '#{@settings["email"]["to"]}' < #{body_file.path}"
359
+ else
360
+ raise "No smtp server (that we can connect to) at #{smtp_host}:#{smtp_port}, could not fall back to /usr/bin/mail either (doesn't exist). Sorry, tried my best."
361
+ end
362
+ end
363
+
364
+ def run args
365
+ @command = args
366
+ action = "wait_on_#{@options[:wait_on].to_s}".to_sym
367
+ if ! self.respond_to? action
368
+ raise "Error: don't know how to wait on: #{@options[:wait_on]}"
369
+ end
370
+
371
+ @start_time = Time.now
372
+ if @options[:write_config_to]
373
+ save_settings @options[:write_config_to]
374
+ end
375
+ save_settings
376
+ self.send action, args
377
+ return 1
378
+ end
379
+ end
380
+
381
+ if $0 == __FILE__
382
+ TellMeWhen.main(ARGV)
383
+ end
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+
3
+
4
+ SPEC = Gem::Specification.new do |s|
5
+ s.name = "tellmewhen"
6
+ s.version = "1.0.0"
7
+ s.author = "Kyle Burton"
8
+ s.email = "kyle.burton@gmail.com"
9
+ s.platform = Gem::Platform::RUBY
10
+ s.description = <<DESC
11
+ Notifys you when another command completes (via email).
12
+
13
+ ./tellmewhen 'sleep 3; ls' # await a command to complete
14
+ ./tellmewhen -p 12345 # await a pid to exit
15
+ ./tellmewhen -e some-file.txt # await existance
16
+ ./tellmewhen -m some-file.txt # await update
17
+
18
+ Tells you when, how long and sends you the output (for commands it runs).
19
+
20
+ DESC
21
+ s.summary = "Notifys you when another command completes (via email)."
22
+ # s.rubyforge_project = "typrtail"
23
+ s.homepage = "http://github.com/kyleburton/tellmewhen"
24
+ s.files = Dir.glob("**/*")
25
+ s.executables << "tellmewhen"
26
+ s.require_path = "bin"
27
+ s.has_rdoc = false
28
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tellmewhen
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Kyle Burton
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-25 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: |+
23
+ Notifys you when another command completes (via email).
24
+
25
+ ./tellmewhen 'sleep 3; ls' # await a command to complete
26
+ ./tellmewhen -p 12345 # await a pid to exit
27
+ ./tellmewhen -e some-file.txt # await existance
28
+ ./tellmewhen -m some-file.txt # await update
29
+
30
+ Tells you when, how long and sends you the output (for commands it runs).
31
+
32
+ email: kyle.burton@gmail.com
33
+ executables:
34
+ - tellmewhen
35
+ extensions: []
36
+
37
+ extra_rdoc_files: []
38
+
39
+ files:
40
+ - bin/tellmewhen
41
+ - Rakefile
42
+ - README.textile
43
+ - tellmewhen.gemspec
44
+ has_rdoc: true
45
+ homepage: http://github.com/kyleburton/tellmewhen
46
+ licenses: []
47
+
48
+ post_install_message:
49
+ rdoc_options: []
50
+
51
+ require_paths:
52
+ - bin
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.7
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Notifys you when another command completes (via email).
78
+ test_files: []
79
+