tellmewhen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+