Permalink
Browse files

Terminator: Added loop saftey, added specs for optional block, update…

…d readme and updated documentation

git-svn-id: svn://rubyforge.org/var/svn/codeforpeople/terminator/trunk@695 29922619-e960-4932-8d85-0e0be7f1775a
  • Loading branch information...
1 parent 48e59d8 commit 81c2ba6bd5612142581b52b42796dc7fe58c729f Mikel Lindsaar committed Sep 20, 2008
Showing with 201 additions and 87 deletions.
  1. +38 −2 README
  2. +1 −1 gen_readme.rb
  3. +67 −40 lib/terminator.rb
  4. +12 −0 samples/e.rb
  5. +83 −44 spec/terminator_spec.rb
  6. BIN terminator-0.4.3.gem
  7. BIN terminator-0.4.4.gem
View
40 README
@@ -5,7 +5,7 @@ SYNOPSIS
An external timeout mechanism based on processes and signals. Safe on
windows. Safe for system calls. Safe for minors. but not very safe
for misbehaving, downtrodden zombied out processes.
-
+
DESCRIPTION
Terminator is a solution to the problem of 'how am I meant to kill a
system call in Ruby!?'
@@ -43,6 +43,13 @@ HOW IT WORKS
So really it is a race of who is going to win?
+ Word of warning though. Terminator is not subtle. Don't expect it to split
+ hairs. Trying to give a process that takes about 1 second to complete, a
+ 2 second terminator... well... odds are 50/50 on who is going to make it...
+
+ If you have a 1 second process, give it 3 seconds to complete. Arnie doesn't
+ much care for casualties of war...
+
INSTALL
gem install terminator
@@ -52,7 +59,13 @@ URIS
HISTORY
0.4.2
- initial version
+ * initial version (ara)
+ 0.4.3
+ * added some extra specs and test cases (mikel)
+ 0.4.4
+ * made terminator loop safe. 1000.times { Terminator.timeout(1) do true; end }
+ now works (mikel)
+ * added more test cases (mikel)
AUTHORS
ara.t.howard - ara.t.howard@gmail.com
@@ -128,3 +141,26 @@ SAMPLES
signaled @ 1221026177
woke up @ 1221026178
+
+ <========< samples/e.rb >========>
+
+ ~ > cat samples/e.rb
+
+ require 'terminator'
+
+ puts "Looping 1000 times on the terminator..."
+ success = false
+ 1.upto(1000) do |i|
+ success = false
+ Terminator.terminate(1) do
+ success = true
+ end
+ print "\b\b\b#{i}"
+ end
+ puts "\nI was successful" if success
+
+ ~ > ruby samples/e.rb
+
+ Looping 1000 times on the terminator...
+ 1000
+ I was successful
View
2 gen_readme.rb
@@ -1,5 +1,5 @@
#! /usr/bin/env ruby
-
+require 'rubygems'
require 'pathname'
$VERBOSE=nil
View
107 lib/terminator.rb
@@ -1,71 +1,100 @@
require 'rbconfig'
-require 'tempfile'
-
require 'fattr'
+# The terminator library is simple to use.
+#
+# require 'terminator'
+# Terminator.terminate(1) do
+# sleep 4
+# puts("I will never print")
+# end
+# #=> Terminator::Error: Timeout out after 1s
+#
+# The above code snippet will raise a Terminator::Error as the terminator's timeout is
+# 2 seconds and the block will take at least 4 to complete.
+#
+# You can put error handling in with a simple begin / rescue block:
+#
+# require 'terminator'
+# begin
+# Terminator.terminate(1) do
+# sleep 4
+# puts("I will never print")
+# end
+# rescue
+# puts("I got terminated")
+# end
+# #=> I got terminated, but rescued myself.
+#
+# The standard action on termination is to raise a Terminator::Error, however, this is
+# just an anonymous object that is called, so you can pass your own trap handling by
+# giving the terminator a lambda as an argument.
+#
+# require 'terminator'
+# custom_trap = lambda { eval("raise(RuntimeError, 'Oops... I failed...')") }
+# Terminator.terminate(:seconds => 1, :trap => custom_trap) do
+# sleep 10
+# end
+# #=> RuntimeError: (eval):1:in `irb_binding': Oops... I failed...
+#
module Terminator
Version = '0.4.3'
+ # Terminator.terminate has two ways you can call it. You can either just specify:
+ #
+ # Terminator.terminate(seconds) { code_to_execute }
+ #
+ # where seconds is an integer number greater than or equal to 1. If you pass a float
+ # in on seconds, Terminator will call to_i on it and convert it to an integer. This
+ # is because Terminator is not a precise tool, due to it calling a new ruby instance,
+ # and spawning a new process, relying on split second accuracy is a folly.
+ #
+ # If you want to pass in the block, please use:
+ #
+ # Terminator.terminate(:seconds => seconds, :trap => block) { code_to_execute }
+ #
+ # Where block is an anonymous method that gets called when the timeout occurs.
def terminate options = {}, &block
- options = { :seconds => Float(options).to_f } unless Hash === options
+ options = { :seconds => Float(options).to_i } unless Hash === options
seconds = getopt :seconds, options
- raise ::Terminator::Error, "Time to kill must be greater than 0" unless seconds > 0
+ raise ::Terminator::Error, "Time to kill must be at least 1 second" unless seconds >= 1
trap = getopt :trap, options, lambda{ eval("raise(::Terminator::Error, 'Timeout out after #{ seconds }s')", block) }
handler = Signal.trap(signal, &trap)
- plot_to_kill pid, :in => seconds, :with => signal
+ terminator_pid = plot_to_kill pid, :in => seconds, :with => signal
begin
block.call
+ nuke_terminator(terminator_pid)
ensure
Signal.trap(signal, handler)
end
end
+ private
+
+ def nuke_terminator(pid)
+ Process.kill("KILL", pid) rescue nil
+ Process.wait(pid)
+ end
+
def plot_to_kill pid, options = {}
seconds = getopt :in, options
signal = getopt :with, options
- process.puts [pid, seconds, signal].join(' ')
- process.flush
+ send_terminator(pid, seconds)
end
- fattr :process do
- process = IO.popen "#{ ruby } #{ program.inspect }", 'w+'
- at_exit do
- begin
- Process.kill -9, process.pid
- rescue Object
- end
- end
- process.sync = true
- process
+ def send_terminator(pid, seconds)
+ process = IO.popen(%[#{ ruby } -e'sleep #{seconds}.to_i; Process.kill("#{signal}", #{pid}) rescue nil;'], 'w+')
+ process.pid
end
- fattr :program do
- code = <<-code
- while(( line = STDIN.gets ))
- pid, seconds, signal, *ignored = line.strip.split
-
- pid = Float(pid).to_i
- seconds = Float(seconds)
- signal = Float(signal).to_i rescue String(signal)
-
- sleep seconds
-
- begin
- Process.kill signal, pid
- rescue Object
- end
- end
- code
- tmp = Tempfile.new "#{ ppid }-#{ pid }-#{ rand }"
- tmp.write code
- tmp.close
- tmp.path
+ def temp_file_name
+ "terminator-#{ ppid }-#{ pid }-#{ rand }"
end
fattr :ruby do
@@ -96,8 +125,6 @@ def getopt key, hash, default = nil
default
end
-
-
class Error < ::StandardError; end
def error() ::Terminator::Error end
def version() ::Terminator::Version end
View
12 samples/e.rb
@@ -0,0 +1,12 @@
+require 'terminator'
+
+puts "Looping 1000 times on the terminator..."
+success = false
+1.upto(1000) do |i|
+ success = false
+ Terminator.terminate(1) do
+ success = true
+ end
+ print "\b\b\b#{i}"
+end
+puts "\nI was successful" if success
View
127 spec/terminator_spec.rb
@@ -8,10 +8,6 @@
describe Terminator do
describe "being given a contract to terminate" do
- it "should not complain about it" do
- doing { Terminator.terminate(1) { "Hello" } }.should_not raise_error
- end
-
it "should not accept an expired contract" do
doing { Terminator.terminate(0) { "Hello" } }.should.raise(Terminator::Error)
end
@@ -20,108 +16,151 @@
doing { Terminator.terminate(-0.1) { "Hello" } }.should.raise(Terminator::Error)
end
- it "should handle fractions of seconts" do
- failed = false
- Terminator.terminate(0.3) do
- failed = true
- end
- failed.should.be.false
+ it "should refuse fractions of seconds less than 1" do
+ doing { Terminator.terminate(0.1) { "Hello" } }.should.raise(Terminator::Error)
end
end
describe "handling contracts" do
it "should not kill it's mark if the mark completes" do
- failed = false
- Terminator.terminate(0.01) do
- failed = true
+ success = false
+ Terminator.terminate(10) do
+ success = true
end
- failed.should.be.false
+ success.should == true
end
it "should not terminate it's mark until the time is up" do
- failed = false
- Terminator.terminate(1) do
- sleep 0.9
- failed = true
+ success = false
+ Terminator.terminate(10) do
+ sleep 0.1
+ success = true
end
- failed.should.be.false
+ success.should == true
end
- it "should handle multiple overlapping contracts gracefully" do
+ it "should handle multiple sequential contracts gracefully" do
first_job = false
second_job = false
third_job = false
- Terminator.terminate(0.3) do
+ Terminator.terminate(10) do
first_job = true
end
- Terminator.terminate(0.3) do
+ Terminator.terminate(10) do
second_job = true
end
- Terminator.terminate(0.3) do
+ Terminator.terminate(10) do
third_job = true
end
- first_job.should.be.true
- second_job.should.be.true
- third_job.should.be.true
+ first_job.should == true
+ second_job.should == true
+ third_job.should == true
+ end
+
+ it "should terminate a process that takes too long" do
+ first_job = false
+
+ begin
+ Terminator.terminate(1) do
+ sleep 10
+ first_job = true
+ end
+ rescue Terminator::Error
+ nil
+ end
+
+ first_job.should == false
end
it "should be a surgical weapon only selectively destroying it's marks" do
first_job = false
second_job = false
begin
- Terminator.terminate(0.3) do
- sleep 0.4
+ Terminator.terminate(1) do
+ sleep 10
first_job = true
end
rescue
nil
end
- Terminator.terminate(0.3) do
+ Terminator.terminate(10) do
second_job = true
end
- first_job.should.be.false
- second_job.should.be.true
+ first_job.should == false
+ second_job.should == true
end
it "should a surgical weapon only selectively destroying it's marks - backwards" do
first_job = false
second_job = false
- Terminator.terminate(0.3) do
+ Terminator.terminate(10) do
first_job = true
end
begin
- Terminator.terminate(0.3) do
- sleep 0.4
+ Terminator.terminate(1) do
+ sleep 10
second_job = true
end
rescue
nil
end
- first_job.should.be.true
- second_job.should.be.false
+ first_job.should == true
+ second_job.should == false
end
- it "should accept an optional trap handler" do
- trap = lambda{ 'You failed me again!' }
-
- doing {
- Terminator.terminate(0.001, :trap => trap) do
- sleep 0.2
- job = true
- end }.should.raise(Terminator::Error)
+ it "should handle many many contracts" do
+ success = false
+ 1000.times do
+ success = false
+ Terminator.terminate(1) do
+ success = true
+ end
+ end
+ success.should == true
+ end
+
+ it "should handle many many contracts with a longer attention span" do
+ success = false
+ 5.times do
+ success = false
+ Terminator.terminate(5) do
+ sleep 1
+ success = true
+ end
+ end
+ success.should == true
+ end
+ it "should handle many many contracts with the last one failing" do
+ sleep_time = 0
+ begin
+ 5.times do
+ Terminator.terminate(2) do
+ sleep sleep_time
+ sleep_time += 1
+ end
+ end
+ rescue Terminator::Error
+ nil
+ end
+ sleep_time.should < 4
+ end
+
+ it "should be able to pass in a block for arbitrary execution" do
+ new_block = lambda { eval("raise(RuntimeError, 'Oops... I failed...')") }
+ doing { Terminator.terminate(:seconds => 1, :trap => new_block) { sleep 10 } }.should.raise(RuntimeError)
end
end
View
BIN terminator-0.4.3.gem
Binary file not shown.
View
BIN terminator-0.4.4.gem
Binary file not shown.

0 comments on commit 81c2ba6

Please sign in to comment.