daikini-immortalize 0.2.6

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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 BehindLogic
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,17 @@
1
+ = immortalize
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 BehindLogic. See LICENSE for details.
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "daikini-immortalize"
8
+ gem.summary = %Q{Restarts a specified process if it dies.}
9
+ gem.description = %Q{Watch a specific process, restart it if it dies.}
10
+ gem.email = "jonathan+rubygems@daikini.com"
11
+ gem.homepage = "http://github.com/daikini/immortalize"
12
+ gem.authors = ["Daikini", "Mutually Human Software", "BehindLogic"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "immortalize #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.6
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Usage:
4
+ # immortalize run "command --with-args --etc"
5
+ # immortalize remove "command --with-args --etc"
6
+ # immortalize inspect
7
+ # immortalize # << run this from a cron job every minute to check and restart failed processes.
8
+ # * starts process
9
+ # * logs the start command with pid
10
+ # * when run from cron (without args), check all logged start commands and currently logged pid to see if it's running
11
+ # * if not running, immediately remove pid from log, but leave command; start process again via: immortalize "command --with-args --etc"
12
+ require 'time'
13
+ require 'yaml'
14
+ require 'sha1'
15
+ require 'ftools'
16
+ require 'optparse'
17
+ $options = {}
18
+
19
+ require 'rubygems'
20
+ require 'mail'
21
+
22
+ optparse = OptionParser.new do |opts|
23
+ opts.banner = <<-ENDBANNER
24
+ Usage: #{$0} [run|remove|inspect] [options]
25
+
26
+ To add (and start) a command:
27
+ #{$0} run "command" --notify admin@email.com --max_failures 5
28
+ To change a command's options, just re-add it.
29
+ You can group commands by adding a --group option, which allows
30
+ you to perform future actions on all commands in the group.
31
+
32
+ To stop a daemon:
33
+ #{$0} stop "command"
34
+ #{$0} stop 1 # <- 1 is an index as in 'immortalize list' below
35
+ #{$0} stop all
36
+
37
+ To remove a command:
38
+ #{$0} remove "command"
39
+ #{$0} remove 1 # <- 1 is an index as in 'immortalize list' below
40
+ #{$0} remove all
41
+
42
+ To inspect the current list of immortal commands:
43
+ #{$0} list
44
+
45
+ To install the immortalize cron job (which does the actual work):
46
+ immortalize setup
47
+
48
+ Run this command with no arguments as a cron job, to run every minute:
49
+ * * * * * immortalize
50
+
51
+ Options:
52
+ ENDBANNER
53
+
54
+ $options[:notify] = nil
55
+ opts.on( '--notify EMAIL', "The email address to which failure notifications should be sent." ) do |email|
56
+ $options[:notify] = email
57
+ end
58
+
59
+ $options[:max_failures] = 5
60
+ opts.on('--max_failures NUM', "Notify on NUM or more failures within an hour (default 5)") do |num|
61
+ $options[:max_failures] = num.to_i
62
+ end
63
+
64
+ $options[:group] = nil
65
+ opts.on('--group GROUP', "Set the group of immortal commands that this command belongs to.") do |group|
66
+ $options[:group] = group
67
+ end
68
+
69
+ $log_location = "#{ENV['HOME']}/.immortalize"
70
+ opts.on('--log-location PATH', "Manually set the location for immortalize to keep its registry and cron.log (default #{$log_location})") do |path|
71
+ if !File.directory?(path)
72
+ warn "`#{path}' is not a valid path."
73
+ exit 1
74
+ end
75
+ $log_location = path
76
+ end
77
+
78
+ opts.on( '-h', '--help', 'Display this screen' ) do
79
+ puts opts
80
+ exit
81
+ end
82
+ end
83
+ optparse.parse!
84
+
85
+
86
+ `mkdir -p "#{$log_location}"` unless File.directory?($log_location)
87
+ $registry_filename = "#{$log_location}/registry.yaml"
88
+ File.open($registry_filename, 'w'){|f| f << {}.to_yaml} unless File.exists?($registry_filename)
89
+ unless $registry = YAML.load_file($registry_filename)
90
+ File.open($registry_filename, 'w'){|f| f << {}.to_yaml}
91
+ $registry = {}
92
+ end
93
+
94
+ $action = ARGV[0]
95
+
96
+ def notify(immortal, message)
97
+ m = Mail.new do
98
+ to immortal[:notify]
99
+ from "immortalize@video.iremix.org"
100
+ subject "ImmortalCommand `#{immortal[:command]}' keeps dying!"
101
+ body message
102
+ end
103
+
104
+ m.delivery_method :sendmail
105
+ m.deliver
106
+ end
107
+
108
+ class Time
109
+ def beginning_of_day
110
+ Time.mktime(year, month, day).send(gmt? ? :gmt : :localtime)
111
+ end
112
+ end
113
+
114
+ class Immortal
115
+ def self.new(identifier)
116
+ if $registry[identifier]
117
+ obj = allocate
118
+ obj.send :initialize, identifier
119
+ obj
120
+ else
121
+ puts "tried:\n\t#{identifier}commands:\n\t#{$registry.values.map {|r| r[:command]}.join("\n\t")}"
122
+ end
123
+ end
124
+
125
+ def self.in_group(group)
126
+ $registry.select {|k,i| i[:group] == group}.collect { |k,i| new(k) }
127
+ end
128
+
129
+ attr_reader :identifier
130
+ def initialize(identifier)
131
+ @identifier = identifier
132
+ @reg = $registry[identifier]
133
+ end
134
+
135
+ def [](key)
136
+ @reg[key]
137
+ end
138
+
139
+ def running?
140
+ @reg[:pid] && `ps #{@reg[:pid]} | grep "#{@reg[:pid]}" | grep -v "grep"` =~ /#{@reg[:pid]}/
141
+ end
142
+
143
+ def failures
144
+ @failures ||= File.exists?(failure_log_file) ? File.readlines(failure_log_file).map {|t| Time.parse(t) } : []
145
+ end
146
+
147
+ def start!
148
+ # Run the command and gather the pid
149
+ pid = nil
150
+ open("|#{@reg[:command]} 1>/dev/null & echo $!") do |f|
151
+ pid = f.sysread(5).chomp.to_i
152
+ end
153
+ # Log the pid
154
+ puts "pid #{pid}"
155
+ $registry[identifier][:pid] = pid
156
+ end
157
+ def stop!
158
+ if running?
159
+ pid = $registry[identifier].delete(:pid)
160
+ `kill -TERM #{pid}`
161
+ puts "attempted to kill pid #{pid}: \"#{@reg[:command]}\""
162
+ else
163
+ warn "\"#{@reg[:command]}\" not running!"
164
+ false
165
+ end
166
+ end
167
+
168
+ def failed!
169
+ failures << Time.now
170
+ File.open(failure_log_file, 'a') do |log|
171
+ log << "#{Time.now}\n"
172
+ end
173
+ end
174
+
175
+ def frequent_failures?
176
+ # If it failed :max_failures times or more within the last hour.
177
+ failures.length >= self[:max_failures] && failures[0-self[:max_failures]] > Time.now - 3600
178
+ end
179
+ def failures_today
180
+ failures.select {|f| f > Time.now.beginning_of_day}
181
+ end
182
+ def failures_this_hour
183
+ failures_today.select {|f| f > Time.now - 3600}
184
+ end
185
+
186
+ def inspect
187
+ self[:command] + (" (group=#{self[:group]})" if self[:group]).to_s + (failures.length >= self[:max_failures].to_i ? "\n\tLast #{self[:max_failures]} failures: #{failures[-5..1].join(", ")}" : '')
188
+ end
189
+
190
+ private
191
+ def failure_log_file
192
+ "#{$log_location}/#{@identifier}"
193
+ end
194
+ end
195
+
196
+ # Curate the command string
197
+ $command_string = nil
198
+ if ARGV[1].to_s.length > 1 && ARGV[1] !~ /^\d+$/ && ARGV[1] != 'all'
199
+ $command_string = ARGV[1]
200
+ # Complain about the string if it does not have proper output redirections
201
+ cmds = $command_string.split(/; ?/)
202
+ last_cmd = cmds.pop
203
+ lst,out_s = last_cmd.split(/(?: \d)? ?>/,2)
204
+ cmds << lst
205
+ outs = last_cmd.scan(/(?: \d)? ?>>? ?\S+/)
206
+ outs = outs.map {|o| o.sub(/^ ?(>>)? ?/,"\\1").sub(/^>/,"1>").sub(/^ /,'') }
207
+ unless outs.any? {|o| o =~ /^2>/}
208
+ warn "Appending default STDERR redirection: 2>&1 (STDERR > STDOUT)"
209
+ outs << "2>&1"
210
+ end
211
+ unless outs.any? {|o| o =~ /^1>/}
212
+ warn "#{$command_string}\nInvalid command: You need to add proper STDOUT redirection, ex: >/dev/null or 1>log/run.log"
213
+ exit
214
+ end
215
+ $command_string = cmds.join('; ') + ' ' + outs.join(' ')
216
+ end
217
+
218
+
219
+ # Main logic
220
+ unless ::Object.const_defined?(:IRB)
221
+ case $action
222
+ when 'setup'
223
+ crons = `crontab -l 2>/dev/null`.split(/\n/)
224
+ crons.reject! {|c| c =~ /immortalize.*>?> #{$log_location}\/cron.log/}
225
+ crons << "* * * * * #{$0} --log-location=\"#{$log_location}\" >> #{$log_location}/cron.log 2>&1\n"
226
+ puts "Installing crons:\n\t#{crons.join("\n\t")}"
227
+ f = IO.popen("crontab -", 'w')
228
+ f << crons.join("\n")
229
+ f.close
230
+ crons = `crontab -l 2>/dev/null`.split(/\n/)
231
+ puts "Installed crons:\n\t#{crons.join("\n\t")}"
232
+ when 'list'
233
+ # puts $registry.inspect
234
+ puts "Immortalized:"
235
+ keys = $registry.keys.sort
236
+ keys.each_with_index do |identifier,i|
237
+ puts "\t#{i+1}) " + Immortal.new(identifier).inspect
238
+ end
239
+ puts "\nTo remove jobs, for example job #1, from this list, run `immortalize remove 1`"
240
+ exit
241
+
242
+ when 'stop'
243
+ if ARGV[1] =~ /^\d+$/
244
+ identifier = $registry.keys.sort[ARGV[1].to_i-1]
245
+ immortal = Immortal.new(identifier)
246
+ immortal.stop!
247
+ elsif ARGV[1] == 'all'
248
+ $registry.keys.each do |identifier|
249
+ immortal = Immortal.new(identifier)
250
+ immortal.stop!
251
+ end
252
+ else
253
+ if $command_string
254
+ identifier = SHA1.hexdigest($command_string)
255
+ immortal = Immortal.new(identifier)
256
+ immortal.stop!
257
+ elsif $options[:group]
258
+ Immortal.in_group($options[:group]).each do |immortal|
259
+ immortal.stop!
260
+ end
261
+ end
262
+ end
263
+
264
+ when 'run'
265
+ if $options[:notify].nil?
266
+ warn "Must include --notify EMAIL_ADDRESS when adding a command!"
267
+ exit
268
+ end
269
+
270
+ # Running with a given command.
271
+ identifier = SHA1.hexdigest($command_string)
272
+
273
+ # Create the command
274
+ $registry[identifier] ||= {
275
+ :command => $command_string,
276
+ :group => $options[:group]
277
+ }
278
+ $registry[identifier].merge!($options)
279
+
280
+ immortal = Immortal.new(identifier)
281
+ # Start the process if it isn't already running
282
+ if immortal.running?
283
+ puts "`#{immortal[:command]}' is already running with pid #{immortal[:pid]}"
284
+ else
285
+ print "Starting `#{immortal[:command]}'... "
286
+ immortal.start!
287
+ end
288
+
289
+ when 'remove'
290
+ if ARGV[1] =~ /^\d+$/
291
+ identifier = $registry.keys.sort[ARGV[1].to_i-1]
292
+ reg = $registry.delete(identifier)
293
+ puts "Deleted #{identifier}: \"#{reg[:command]}\""
294
+ elsif ARGV[1] == 'all'
295
+ $registry.keys.each do |identifier|
296
+ reg = $registry.delete(identifier)
297
+ puts "Deleted #{identifier}: \"#{reg[:command]}\""
298
+ end
299
+ else
300
+ if $command_string
301
+ identifier = SHA1.hexdigest($command_string)
302
+ reg = $registry.delete(identifier)
303
+ puts "Deleted #{identifier}: \"#{reg[:command]}\""
304
+ elsif $options[:group]
305
+ Immortal.in_group($options[:group]).each do |immortal|
306
+ reg = $registry.delete(immortal.identifier)
307
+ puts "Deleted #{immortal.identifier}: \"#{reg[:command]}\""
308
+ end
309
+ end
310
+ end
311
+
312
+ when nil
313
+ # Running bare from cron.
314
+ # Check all logged commands with pids.
315
+ puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] #{$registry.length} jobs"
316
+ $registry.keys.sort.each_with_index do |identifier,i|
317
+ immortal = Immortal.new(identifier)
318
+
319
+ # Check if running
320
+ if immortal.running?
321
+ # puts " #{i+1}) `#{immortal[:command]}' is running fine..."
322
+ else
323
+ puts " #{i+1}) `#{immortal[:command]}' HAS DIED! Reviving..."
324
+ # Mark the failure
325
+ immortal.failed!
326
+ # Notify if failures have been frequent
327
+ if immortal.frequent_failures?
328
+ puts " #{i+1}) FREQUENT FAILURE ON #{identifier} (`#{immortal[:command]}')"
329
+ notify(immortal, "ImmortalCommand failure!\n\nCommand `#{immortal[:command]}' failed at #{Time.now}, threshold is #{immortal[:max_failures]} / hour.\n\n#{immortal.failures_today.size} failures so far today, #{immortal.failures_this_hour.size} in the past hour.")
330
+ end
331
+ # Start it
332
+ immortal.start!
333
+ end
334
+ end
335
+ else
336
+ puts optparse
337
+ end
338
+ end
339
+
340
+ # Save the registry
341
+ File.open("#{$log_location}/registry.yaml~", 'w') do |r|
342
+ r << $registry.to_yaml
343
+ end
344
+ # Swap files back out IF registry was written correctly.
345
+ if File.read("#{$log_location}/registry.yaml~") == $registry.to_yaml
346
+ File.delete("#{$log_location}/registry.yaml")
347
+ File.move("#{$log_location}/registry.yaml~", "#{$log_location}/registry.yaml")
348
+ else
349
+ raise "PROBLEM WRITING TO #{$log_location}/registry.yaml!"
350
+ end
@@ -0,0 +1,47 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{daikini-immortalize}
8
+ s.version = "0.2.6"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Daikini", "Mutually Human Software", "BehindLogic"]
12
+ s.date = %q{2010-09-27}
13
+ s.default_executable = %q{immortalize}
14
+ s.description = %q{Watch a specific process, restart it if it dies.}
15
+ s.email = %q{jonathan+rubygems@daikini.com}
16
+ s.executables = ["immortalize"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".gitignore",
24
+ "LICENSE",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "bin/immortalize",
29
+ "daikini-immortalize.gemspec"
30
+ ]
31
+ s.homepage = %q{http://github.com/daikini/immortalize}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = %q{1.3.7}
35
+ s.summary = %q{Restarts a specified process if it dies.}
36
+
37
+ if s.respond_to? :specification_version then
38
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
39
+ s.specification_version = 3
40
+
41
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
42
+ else
43
+ end
44
+ else
45
+ end
46
+ end
47
+
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: daikini-immortalize
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 6
10
+ version: 0.2.6
11
+ platform: ruby
12
+ authors:
13
+ - Daikini
14
+ - Mutually Human Software
15
+ - BehindLogic
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2010-09-27 00:00:00 -06:00
21
+ default_executable: immortalize
22
+ dependencies: []
23
+
24
+ description: Watch a specific process, restart it if it dies.
25
+ email: jonathan+rubygems@daikini.com
26
+ executables:
27
+ - immortalize
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - LICENSE
32
+ - README.rdoc
33
+ files:
34
+ - .document
35
+ - .gitignore
36
+ - LICENSE
37
+ - README.rdoc
38
+ - Rakefile
39
+ - VERSION
40
+ - bin/immortalize
41
+ - daikini-immortalize.gemspec
42
+ has_rdoc: true
43
+ homepage: http://github.com/daikini/immortalize
44
+ licenses: []
45
+
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --charset=UTF-8
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.7
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Restarts a specified process if it dies.
76
+ test_files: []
77
+