krakatoa-icmp4em 0.0.3
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.
- data/Gemfile +15 -0
- data/Gemfile.lock +30 -0
- data/README +0 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/examples/simple_example.rb +22 -0
- data/examples/stateful_example.rb +22 -0
- data/krakatoa-icmp4em.gemspec +65 -0
- data/lib/icmp4em.rb +6 -0
- data/lib/icmp4em/common.rb +140 -0
- data/lib/icmp4em/handler.rb +53 -0
- data/lib/icmp4em/icmpv4.rb +174 -0
- metadata +130 -0
data/Gemfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
source "http://rubygems.org"
|
|
2
|
+
# Add dependencies required to use your gem here.
|
|
3
|
+
# Example:
|
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
|
5
|
+
|
|
6
|
+
gem "eventmachine", ">= 1.0.0.beta.4"
|
|
7
|
+
|
|
8
|
+
# Add dependencies to develop your gem here.
|
|
9
|
+
# Include everything needed to run rake, tests, features, etc.
|
|
10
|
+
group :development do
|
|
11
|
+
gem "shoulda", ">= 0"
|
|
12
|
+
gem "bundler", "~> 1.1.0"
|
|
13
|
+
gem "jeweler", "~> 1.6.4"
|
|
14
|
+
gem "simplecov"
|
|
15
|
+
end
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: http://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
eventmachine (1.0.0.beta.4)
|
|
5
|
+
git (1.2.5)
|
|
6
|
+
jeweler (1.6.4)
|
|
7
|
+
bundler (~> 1.0)
|
|
8
|
+
git (>= 1.2.5)
|
|
9
|
+
rake
|
|
10
|
+
multi_json (1.1.0)
|
|
11
|
+
rake (0.9.2.2)
|
|
12
|
+
shoulda (3.0.1)
|
|
13
|
+
shoulda-context (~> 1.0.0)
|
|
14
|
+
shoulda-matchers (~> 1.0.0)
|
|
15
|
+
shoulda-context (1.0.0)
|
|
16
|
+
shoulda-matchers (1.0.0)
|
|
17
|
+
simplecov (0.6.1)
|
|
18
|
+
multi_json (~> 1.0)
|
|
19
|
+
simplecov-html (~> 0.5.3)
|
|
20
|
+
simplecov-html (0.5.3)
|
|
21
|
+
|
|
22
|
+
PLATFORMS
|
|
23
|
+
ruby
|
|
24
|
+
|
|
25
|
+
DEPENDENCIES
|
|
26
|
+
bundler (~> 1.1.0)
|
|
27
|
+
eventmachine (>= 1.0.0.beta.4)
|
|
28
|
+
jeweler (~> 1.6.4)
|
|
29
|
+
shoulda
|
|
30
|
+
simplecov
|
data/README
ADDED
|
File without changes
|
data/Rakefile
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'bundler'
|
|
5
|
+
begin
|
|
6
|
+
Bundler.setup(:default, :development)
|
|
7
|
+
rescue Bundler::BundlerError => e
|
|
8
|
+
$stderr.puts e.message
|
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
|
10
|
+
exit e.status_code
|
|
11
|
+
end
|
|
12
|
+
require 'rake'
|
|
13
|
+
|
|
14
|
+
require 'jeweler'
|
|
15
|
+
Jeweler::Tasks.new do |gem|
|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
|
17
|
+
gem.name = "krakatoa-icmp4em"
|
|
18
|
+
gem.homepage = "http://github.com/krakatoa/icmp4em"
|
|
19
|
+
gem.license = "MIT"
|
|
20
|
+
gem.summary = %Q{Asynchronous implementation of ICMP ping over EventMachine}
|
|
21
|
+
gem.description = %Q{Asynchronous implementation of ICMP ping using EventMachine. Can be used to ping many hosts at once in a non-blocking fashion, with callbacks for success, timeout, and host failure/recovery based on specified threshold numbers.}
|
|
22
|
+
gem.email = "krakatoa1987@gmail.com"
|
|
23
|
+
gem.authors = ["Jake Douglas", "Fernando Alonso"]
|
|
24
|
+
gem.add_dependency "eventmachine", ">= 1.0.0.beta.4"
|
|
25
|
+
gem.require_paths = ["lib"]
|
|
26
|
+
# dependencies defined in Gemfile
|
|
27
|
+
end
|
|
28
|
+
Jeweler::RubygemsDotOrgTasks.new
|
|
29
|
+
|
|
30
|
+
require 'rake/testtask'
|
|
31
|
+
Rake::TestTask.new(:test) do |test|
|
|
32
|
+
test.libs << 'lib' << 'test'
|
|
33
|
+
test.pattern = 'test/**/test_*.rb'
|
|
34
|
+
test.verbose = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#require 'rcov/rcovtask'
|
|
38
|
+
#Rcov::RcovTask.new do |test|
|
|
39
|
+
# test.libs << 'test'
|
|
40
|
+
# test.pattern = 'test/**/test_*.rb'
|
|
41
|
+
# test.verbose = true
|
|
42
|
+
# test.rcov_opts << '--exclude "gems/*"'
|
|
43
|
+
#end
|
|
44
|
+
|
|
45
|
+
task :default => :test
|
|
46
|
+
|
|
47
|
+
require 'rake/rdoctask'
|
|
48
|
+
Rake::RDocTask.new do |rdoc|
|
|
49
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
|
50
|
+
|
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
52
|
+
rdoc.title = "bliss #{version}"
|
|
53
|
+
rdoc.rdoc_files.include('README*')
|
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
55
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.3
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'icmp4em'
|
|
3
|
+
|
|
4
|
+
# This is an example of non-stateful usage. Only the callbacks provided in on_success and on_expire are used,
|
|
5
|
+
# and the object does not keep track of up/down or execute callbacks on_failure/on_recovery.
|
|
6
|
+
# The data string can be set to anything, as long as the total packet size is less than MTU of the network.
|
|
7
|
+
|
|
8
|
+
pings = []
|
|
9
|
+
pings << ICMP4EM::ICMPv4.new("google.com")
|
|
10
|
+
pings << ICMP4EM::ICMPv4.new("slashdot.org")
|
|
11
|
+
pings << ICMP4EM::ICMPv4.new("10.99.99.99") # host that will not respond.
|
|
12
|
+
|
|
13
|
+
Signal.trap("INT") { EventMachine::stop_event_loop }
|
|
14
|
+
|
|
15
|
+
EM.run {
|
|
16
|
+
pings.each do |ping|
|
|
17
|
+
ping.data = "bar of foozz"
|
|
18
|
+
ping.on_success {|host, seq, latency| puts "SUCCESS from #{host}, sequence number #{seq}, Latency #{latency}ms"}
|
|
19
|
+
ping.on_expire {|host, seq, exception| puts "FAILURE from #{host}, sequence number #{seq}, Reason: #{exception.to_s}"}
|
|
20
|
+
ping.schedule
|
|
21
|
+
end
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'icmp4em'
|
|
3
|
+
|
|
4
|
+
# This example shows stateful usage, which tracks up/down state of the host based on consecutive number
|
|
5
|
+
# of successful or failing pings specified in failures_required and recoveries_required. Hosts start in
|
|
6
|
+
# 'up' state.
|
|
7
|
+
|
|
8
|
+
pings = []
|
|
9
|
+
pings << ICMP4EM::ICMPv4.new("google.com", :stateful => true)
|
|
10
|
+
pings << ICMP4EM::ICMPv4.new("10.1.0.175", :stateful => true) # host that will not respond.
|
|
11
|
+
|
|
12
|
+
Signal.trap("INT") { EventMachine::stop_event_loop }
|
|
13
|
+
|
|
14
|
+
EM.run {
|
|
15
|
+
pings.each do |ping|
|
|
16
|
+
ping.on_success {|host, seq, latency, count_to_recovery| puts "SUCCESS from #{host}, sequence number #{seq}, Latency #{latency}ms, Recovering in #{count_to_recovery} more"}
|
|
17
|
+
ping.on_expire {|host, seq, exception, count_to_failure| puts "FAILURE from #{host}, sequence number #{seq}, Reason: #{exception.to_s}, Failing in #{count_to_failure} more"}
|
|
18
|
+
ping.on_failure {|host| puts "HOST STATE WENT TO DOWN: #{host} at #{Time.now}"}
|
|
19
|
+
ping.on_recovery {|host| puts "HOST STATE WENT TO UP: #{host} at #{Time.now}"}
|
|
20
|
+
ping.schedule
|
|
21
|
+
end
|
|
22
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Generated by jeweler
|
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
|
+
# -*- encoding: utf-8 -*-
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |s|
|
|
7
|
+
s.name = "krakatoa-icmp4em"
|
|
8
|
+
s.version = "0.0.3"
|
|
9
|
+
|
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
|
+
s.authors = ["Jake Douglas", "Fernando Alonso"]
|
|
12
|
+
s.date = "2012-03-23"
|
|
13
|
+
s.description = "Asynchronous implementation of ICMP ping using EventMachine. Can be used to ping many hosts at once in a non-blocking fashion, with callbacks for success, timeout, and host failure/recovery based on specified threshold numbers."
|
|
14
|
+
s.email = "krakatoa1987@gmail.com"
|
|
15
|
+
s.extra_rdoc_files = [
|
|
16
|
+
"README"
|
|
17
|
+
]
|
|
18
|
+
s.files = [
|
|
19
|
+
"Gemfile",
|
|
20
|
+
"Gemfile.lock",
|
|
21
|
+
"README",
|
|
22
|
+
"Rakefile",
|
|
23
|
+
"VERSION",
|
|
24
|
+
"examples/simple_example.rb",
|
|
25
|
+
"examples/stateful_example.rb",
|
|
26
|
+
"krakatoa-icmp4em.gemspec",
|
|
27
|
+
"lib/icmp4em.rb",
|
|
28
|
+
"lib/icmp4em/common.rb",
|
|
29
|
+
"lib/icmp4em/handler.rb",
|
|
30
|
+
"lib/icmp4em/icmpv4.rb"
|
|
31
|
+
]
|
|
32
|
+
s.homepage = "http://github.com/krakatoa/icmp4em"
|
|
33
|
+
s.licenses = ["MIT"]
|
|
34
|
+
s.require_paths = ["lib"]
|
|
35
|
+
s.rubygems_version = "1.8.10"
|
|
36
|
+
s.summary = "Asynchronous implementation of ICMP ping over EventMachine"
|
|
37
|
+
|
|
38
|
+
if s.respond_to? :specification_version then
|
|
39
|
+
s.specification_version = 3
|
|
40
|
+
|
|
41
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
|
42
|
+
s.add_runtime_dependency(%q<eventmachine>, [">= 1.0.0.beta.4"])
|
|
43
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
|
44
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.1.0"])
|
|
45
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
|
46
|
+
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
|
47
|
+
s.add_runtime_dependency(%q<eventmachine>, [">= 1.0.0.beta.4"])
|
|
48
|
+
else
|
|
49
|
+
s.add_dependency(%q<eventmachine>, [">= 1.0.0.beta.4"])
|
|
50
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
|
51
|
+
s.add_dependency(%q<bundler>, ["~> 1.1.0"])
|
|
52
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
|
53
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
|
54
|
+
s.add_dependency(%q<eventmachine>, [">= 1.0.0.beta.4"])
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
s.add_dependency(%q<eventmachine>, [">= 1.0.0.beta.4"])
|
|
58
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
|
59
|
+
s.add_dependency(%q<bundler>, ["~> 1.1.0"])
|
|
60
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
|
61
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
|
62
|
+
s.add_dependency(%q<eventmachine>, [">= 1.0.0.beta.4"])
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
data/lib/icmp4em.rb
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module ICMP4EM
|
|
2
|
+
|
|
3
|
+
class Timeout < Exception; end;
|
|
4
|
+
|
|
5
|
+
module Common
|
|
6
|
+
|
|
7
|
+
ICMP_ECHOREPLY = 0
|
|
8
|
+
ICMP_ECHO = 8
|
|
9
|
+
ICMP_SUBCODE = 0
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Perform a checksum on the message. This is the sum of all the short
|
|
14
|
+
# words and it folds the high order bits into the low order bits.
|
|
15
|
+
# (This method was stolen directly from net-ping - yaki)
|
|
16
|
+
def generate_checksum(msg)
|
|
17
|
+
length = msg.length
|
|
18
|
+
num_short = length / 2
|
|
19
|
+
check = 0
|
|
20
|
+
|
|
21
|
+
msg.unpack("n#{num_short}").each do |short|
|
|
22
|
+
check += short
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if length % 2 > 0
|
|
26
|
+
check += msg[length-1, 1].unpack('C').first << 8
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
check = (check >> 16) + (check & 0xffff)
|
|
30
|
+
return (~((check >> 16) + check) & 0xffff)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module HostCommon
|
|
36
|
+
|
|
37
|
+
# Set failure callback. The provided Proc or block will be called and yielded the host and sequence number, whenever the failure count exceeds the defined threshold.
|
|
38
|
+
def on_failure(proc = nil, &block)
|
|
39
|
+
@failure = proc || block unless proc.nil? and block.nil?
|
|
40
|
+
@failure
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set recovery callback. The provided Proc or block will be called and yielded the host and sequence number, whenever the recovery count exceeds the defined threshold.
|
|
44
|
+
def on_recovery(proc = nil, &block)
|
|
45
|
+
@recovery = proc || block unless proc.nil? and block.nil?
|
|
46
|
+
@recovery
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Set success callback. This will be called and yielded the host, sequence number, and latency every time a ping returns successfully.
|
|
50
|
+
def on_success(proc = nil, &block)
|
|
51
|
+
@success = proc || block unless proc.nil? and block.nil?
|
|
52
|
+
@success
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Set 'expiry' callback. This will be called and yielded the host, sequence number, and Exception every time a ping fails.
|
|
56
|
+
# This is not just for timeouts! This can be triggered by failure of the ping for any reason.
|
|
57
|
+
def on_expire(proc = nil, &block)
|
|
58
|
+
@expiry = proc || block unless proc.nil? and block.nil?
|
|
59
|
+
@expiry
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Set the number of consecutive 'failure' pings required to switch host state to 'down' and trigger failure callback, assuming the host is up.
|
|
63
|
+
def failures_required=(failures)
|
|
64
|
+
@failures_required = failures
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Set the number of consecutive 'recovery' pings required to switch host state to 'up' and trigger recovery callback, assuming the host is down.
|
|
68
|
+
def recoveries_required=(recoveries)
|
|
69
|
+
@recoveries_required = recoveries
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def success(seq, latency)
|
|
75
|
+
if @success
|
|
76
|
+
if @stateful
|
|
77
|
+
count_to_recover = @up ? 0 : @recoveries_required - @failcount.abs
|
|
78
|
+
@success.call(@host, seq, latency, count_to_recover)
|
|
79
|
+
else
|
|
80
|
+
@success.call(@host, seq, latency)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def expiry(seq, reason)
|
|
86
|
+
if @expiry
|
|
87
|
+
if @stateful
|
|
88
|
+
count_to_fail = @up ? @failures_required - @failcount : 0
|
|
89
|
+
@expiry.call(@host, seq, reason, count_to_fail)
|
|
90
|
+
else
|
|
91
|
+
@expiry.call(@host, seq, reason)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Executes specified failure callback, passing the host to the block.
|
|
97
|
+
def fail
|
|
98
|
+
@failure.call(@host) if @failure
|
|
99
|
+
@up = false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Executes specified recovery callback, passing the host to the block.
|
|
103
|
+
def recover
|
|
104
|
+
@recovery.call(@host) if @recovery
|
|
105
|
+
@up = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Trigger failure/recovery if either threshold is exceeded...
|
|
109
|
+
def check_for_fail_or_recover
|
|
110
|
+
if @failcount > 0
|
|
111
|
+
fail if @failcount >= @failures_required && @up
|
|
112
|
+
elsif @failcount <= -1
|
|
113
|
+
recover if @failcount.abs >= @recoveries_required && !@up
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Adjusts the failure counter after each ping. The failure counter is incremented positively to count failures,
|
|
118
|
+
# and decremented into negative numbers to indicate successful pings towards recovery after a failure.
|
|
119
|
+
# This is an awful mess..just like the rest of this file.
|
|
120
|
+
def adjust_failure_count(direction)
|
|
121
|
+
if direction == :down
|
|
122
|
+
if @failcount > -1
|
|
123
|
+
@failcount += 1
|
|
124
|
+
elsif @failcount <= -1
|
|
125
|
+
@failcount = 1
|
|
126
|
+
end
|
|
127
|
+
elsif direction == :up && !@up
|
|
128
|
+
if @failcount > 0
|
|
129
|
+
@failcount = -1
|
|
130
|
+
elsif @failcount <= -1
|
|
131
|
+
@failcount -= 1
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
@failcount = 0
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module ICMP4EM
|
|
2
|
+
|
|
3
|
+
module Handler
|
|
4
|
+
|
|
5
|
+
include Common
|
|
6
|
+
|
|
7
|
+
def initialize(socket)
|
|
8
|
+
@socket = socket
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def notify_readable
|
|
12
|
+
receive(@socket)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unbind
|
|
16
|
+
@socket.close if @socket
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def receive(socket)
|
|
22
|
+
# The data was available now
|
|
23
|
+
time = Time.now
|
|
24
|
+
# Get data
|
|
25
|
+
host, data = read_socket(socket)
|
|
26
|
+
# Rebuild message array
|
|
27
|
+
msg = data[20,30].unpack("C2 n3 A*")
|
|
28
|
+
# Verify the packet type is echo reply and verify integrity against the checksum it provided
|
|
29
|
+
return unless msg.first == ICMP_ECHOREPLY && verify_checksum?(msg)
|
|
30
|
+
# Find which object it is supposed to go to
|
|
31
|
+
recipient = ICMPv4.instances[msg[3]]
|
|
32
|
+
# Send time and seq number to recipient object
|
|
33
|
+
recipient.send(:receive, msg[4], time) unless recipient.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_socket(socket)
|
|
37
|
+
# Recieve a common MTU, 1500 bytes.
|
|
38
|
+
data, sender = socket.recvfrom(1500)
|
|
39
|
+
# Get the host in case we want to use that later.
|
|
40
|
+
host = Socket.unpack_sockaddr_in(sender).last
|
|
41
|
+
[host, data]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def verify_checksum?(ary)
|
|
45
|
+
cs = ary[2]
|
|
46
|
+
ary_copy = ary.dup
|
|
47
|
+
ary_copy[2] = 0
|
|
48
|
+
cs == generate_checksum(ary_copy.pack("C2 n3 A*"))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
module ICMP4EM
|
|
2
|
+
|
|
3
|
+
class ICMPv4
|
|
4
|
+
|
|
5
|
+
include Common
|
|
6
|
+
include HostCommon
|
|
7
|
+
|
|
8
|
+
@instances = {}
|
|
9
|
+
@recvsocket = nil
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
|
|
13
|
+
attr_reader :instances
|
|
14
|
+
attr_accessor :recvsocket, :handler
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_accessor :bind_host, :interval, :threshold, :timeout, :data, :block
|
|
19
|
+
attr_reader :id, :failures_required, :recoveries_required, :seq
|
|
20
|
+
|
|
21
|
+
# Create a new ICMP object (host). This takes a host and an optional hash of options for modifying the behavior. They are:
|
|
22
|
+
# * :bind_host
|
|
23
|
+
# Bind the socket to this address. The operating system will figure this out on it's own unless you need to set it for a special situation.
|
|
24
|
+
# * :timeout
|
|
25
|
+
# Timeout, in seconds, before the ping is considered expired and the appropriate callbacks are executed. This should be a numeric class.
|
|
26
|
+
# * :block
|
|
27
|
+
# True or false, default is false. True enables a blocking receive mode, for when accurate latency measurement is important. Due to the nature
|
|
28
|
+
# of event loop architecture, a noticable delay in latency can be added when other things are going on in the reactor.
|
|
29
|
+
# * :interval
|
|
30
|
+
# Interval, in seconds, for how often the ping should be sent. Should be a numeric class.
|
|
31
|
+
# * :stateful
|
|
32
|
+
# True or false, default is false. Indicates whether or not this ping object should keep track of it's successes and failures and execute
|
|
33
|
+
# the on_failure/on_recovery callbacks when the specified limits are hit.
|
|
34
|
+
# * :failures_required
|
|
35
|
+
# Indicates how many consequtive failures are required to switch to the 'failed' state and execute the on_failure callback. Applies only when :stateful => true
|
|
36
|
+
# * :recoveries_required
|
|
37
|
+
# Indicates how many consequtive successes are required to switch to the 'recovered' state and execute the on_recovery callback. Applies only when :stateful => true
|
|
38
|
+
def initialize(host, options = {})
|
|
39
|
+
raise 'requires root privileges' if Process.euid > 0
|
|
40
|
+
@host = host
|
|
41
|
+
@ipv4_sockaddr = Socket.pack_sockaddr_in(0, @host)
|
|
42
|
+
@interval = options[:interval] || 1
|
|
43
|
+
@timeout = options[:timeout] || 1
|
|
44
|
+
@stateful = options[:stateful] || false
|
|
45
|
+
@bind_host = options[:bind_host] || nil
|
|
46
|
+
@block = options[:block] || false
|
|
47
|
+
@recoveries_required = options[:recoveries_required] || 5
|
|
48
|
+
@failures_required = options[:failures_required] || 5
|
|
49
|
+
@up = true
|
|
50
|
+
@waiting = {}
|
|
51
|
+
set_id
|
|
52
|
+
@seq, @failcount = 0, 0
|
|
53
|
+
@data = "Ping from EventMachine"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# This must be called when the object will no longer be used, to remove
|
|
57
|
+
# the object from the class variable hash that is searched for recipients when
|
|
58
|
+
# an ICMP echo comes in. Also cancels the periodic timer. Better way to do this whole thing?...
|
|
59
|
+
def stop
|
|
60
|
+
@ptimer.cancel if @ptimer
|
|
61
|
+
self.class.instances[@id] = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Send the echo request to @host and add sequence number to the waiting queue.
|
|
65
|
+
def ping
|
|
66
|
+
raise "EM not running" unless EM.reactor_running?
|
|
67
|
+
init_handler if self.class.recvsocket.nil?
|
|
68
|
+
@seq = ping_send
|
|
69
|
+
if @block
|
|
70
|
+
blocking_receive
|
|
71
|
+
else
|
|
72
|
+
EM.add_timer(@timeout) { self.send(:expire, @seq, Timeout.new("Ping timed out")) } unless @timeout == 0
|
|
73
|
+
end
|
|
74
|
+
@seq
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Uses a periodic timer to ping the host at @interval.
|
|
78
|
+
def schedule
|
|
79
|
+
raise "EM not running" unless EM.reactor_running?
|
|
80
|
+
@ptimer = EM::PeriodicTimer.new(@interval) { self.ping }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Expire a sequence number from the waiting queue.
|
|
86
|
+
# Should only be called by the timer setup in #ping or the rescue Exception in #ping_send.
|
|
87
|
+
def expire(seq, exception = nil)
|
|
88
|
+
waiting = @waiting[seq]
|
|
89
|
+
if waiting
|
|
90
|
+
@waiting[seq] = nil
|
|
91
|
+
adjust_failure_count(:down) if @stateful
|
|
92
|
+
expiry(seq, exception)
|
|
93
|
+
check_for_fail_or_recover if @stateful
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Should only be called by the Handler. Passes the receive time and sequence number.
|
|
98
|
+
def receive(seq, time)
|
|
99
|
+
waiting = @waiting[seq]
|
|
100
|
+
if waiting
|
|
101
|
+
latency = (time - waiting) * 1000
|
|
102
|
+
adjust_failure_count(:up) if @stateful
|
|
103
|
+
success(seq, latency)
|
|
104
|
+
check_for_fail_or_recover if @stateful
|
|
105
|
+
@waiting[seq] = nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Construct and send the ICMP echo request packet.
|
|
110
|
+
def ping_send
|
|
111
|
+
seq = (@seq + 1) % 65536
|
|
112
|
+
|
|
113
|
+
socket = self.class.recvsocket
|
|
114
|
+
|
|
115
|
+
# Generate msg with checksum
|
|
116
|
+
msg = [ICMP_ECHO, ICMP_SUBCODE, 0, @id, seq, @data].pack("C2 n3 A*")
|
|
117
|
+
msg[2..3] = [generate_checksum(msg)].pack('n')
|
|
118
|
+
|
|
119
|
+
# Enqueue so we can expire properly if there is an exception raised during #send
|
|
120
|
+
@waiting[seq] = Time.now
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
# Fire it off
|
|
124
|
+
socket.send(msg, 0, @ipv4_sockaddr)
|
|
125
|
+
# Re-enqueue AFTER sendto() returns. This ensures we aren't adding latency if the socket blocks.
|
|
126
|
+
@waiting[seq] = Time.now
|
|
127
|
+
# Return sequence number to caller
|
|
128
|
+
seq
|
|
129
|
+
rescue Exception => err
|
|
130
|
+
expire(seq, err)
|
|
131
|
+
seq
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Initialize the receiving socket and handler for incoming ICMP packets.
|
|
136
|
+
def init_handler
|
|
137
|
+
self.class.recvsocket = Socket.new(
|
|
138
|
+
Socket::PF_INET,
|
|
139
|
+
Socket::SOCK_RAW,
|
|
140
|
+
Socket::IPPROTO_ICMP
|
|
141
|
+
)
|
|
142
|
+
if @bind_host
|
|
143
|
+
saddr = Socket.pack_sockaddr_in(0, @bind_host)
|
|
144
|
+
self.class.recvsocket.bind(saddr)
|
|
145
|
+
end
|
|
146
|
+
self.class.handler = EM.watch self.class.recvsocket, Handler, self.class.recvsocket
|
|
147
|
+
self.class.handler.notify_readable = true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Sets the instance id to a unique 16 bit integer so it can fit inside relevent the ICMP field.
|
|
151
|
+
# Also adds self to the pool so that incoming messages that it requested can be delivered.
|
|
152
|
+
def set_id
|
|
153
|
+
while @id.nil?
|
|
154
|
+
id = rand(65535)
|
|
155
|
+
unless self.class.instances[id]
|
|
156
|
+
@id = id
|
|
157
|
+
self.class.instances[@id] = self
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def blocking_receive
|
|
163
|
+
r = select([self.class.recvsocket], nil, nil, @timeout)
|
|
164
|
+
|
|
165
|
+
if r and r.first.include?(self.class.recvsocket)
|
|
166
|
+
self.class.handler.notify_readable
|
|
167
|
+
else
|
|
168
|
+
expire(@seq, Timeout.new("Ping timed out"))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: krakatoa-icmp4em
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.3
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Jake Douglas
|
|
9
|
+
- Fernando Alonso
|
|
10
|
+
autorequire:
|
|
11
|
+
bindir: bin
|
|
12
|
+
cert_chain: []
|
|
13
|
+
date: 2012-03-23 00:00:00.000000000 Z
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: eventmachine
|
|
17
|
+
requirement: &27867940 !ruby/object:Gem::Requirement
|
|
18
|
+
none: false
|
|
19
|
+
requirements:
|
|
20
|
+
- - ! '>='
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 1.0.0.beta.4
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: *27867940
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: shoulda
|
|
28
|
+
requirement: &27867460 !ruby/object:Gem::Requirement
|
|
29
|
+
none: false
|
|
30
|
+
requirements:
|
|
31
|
+
- - ! '>='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: *27867460
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: bundler
|
|
39
|
+
requirement: &27866980 !ruby/object:Gem::Requirement
|
|
40
|
+
none: false
|
|
41
|
+
requirements:
|
|
42
|
+
- - ~>
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: 1.1.0
|
|
45
|
+
type: :development
|
|
46
|
+
prerelease: false
|
|
47
|
+
version_requirements: *27866980
|
|
48
|
+
- !ruby/object:Gem::Dependency
|
|
49
|
+
name: jeweler
|
|
50
|
+
requirement: &27866500 !ruby/object:Gem::Requirement
|
|
51
|
+
none: false
|
|
52
|
+
requirements:
|
|
53
|
+
- - ~>
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 1.6.4
|
|
56
|
+
type: :development
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: *27866500
|
|
59
|
+
- !ruby/object:Gem::Dependency
|
|
60
|
+
name: simplecov
|
|
61
|
+
requirement: &27866020 !ruby/object:Gem::Requirement
|
|
62
|
+
none: false
|
|
63
|
+
requirements:
|
|
64
|
+
- - ! '>='
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: *27866020
|
|
70
|
+
- !ruby/object:Gem::Dependency
|
|
71
|
+
name: eventmachine
|
|
72
|
+
requirement: &27865540 !ruby/object:Gem::Requirement
|
|
73
|
+
none: false
|
|
74
|
+
requirements:
|
|
75
|
+
- - ! '>='
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 1.0.0.beta.4
|
|
78
|
+
type: :runtime
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: *27865540
|
|
81
|
+
description: Asynchronous implementation of ICMP ping using EventMachine. Can be used
|
|
82
|
+
to ping many hosts at once in a non-blocking fashion, with callbacks for success,
|
|
83
|
+
timeout, and host failure/recovery based on specified threshold numbers.
|
|
84
|
+
email: krakatoa1987@gmail.com
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files:
|
|
88
|
+
- README
|
|
89
|
+
files:
|
|
90
|
+
- Gemfile
|
|
91
|
+
- Gemfile.lock
|
|
92
|
+
- README
|
|
93
|
+
- Rakefile
|
|
94
|
+
- VERSION
|
|
95
|
+
- examples/simple_example.rb
|
|
96
|
+
- examples/stateful_example.rb
|
|
97
|
+
- krakatoa-icmp4em.gemspec
|
|
98
|
+
- lib/icmp4em.rb
|
|
99
|
+
- lib/icmp4em/common.rb
|
|
100
|
+
- lib/icmp4em/handler.rb
|
|
101
|
+
- lib/icmp4em/icmpv4.rb
|
|
102
|
+
homepage: http://github.com/krakatoa/icmp4em
|
|
103
|
+
licenses:
|
|
104
|
+
- MIT
|
|
105
|
+
post_install_message:
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
none: false
|
|
111
|
+
requirements:
|
|
112
|
+
- - ! '>='
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
segments:
|
|
116
|
+
- 0
|
|
117
|
+
hash: 701688465436271762
|
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
|
+
none: false
|
|
120
|
+
requirements:
|
|
121
|
+
- - ! '>='
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubyforge_project:
|
|
126
|
+
rubygems_version: 1.8.10
|
|
127
|
+
signing_key:
|
|
128
|
+
specification_version: 3
|
|
129
|
+
summary: Asynchronous implementation of ICMP ping over EventMachine
|
|
130
|
+
test_files: []
|