Skip to content
This repository has been archived by the owner on Feb 18, 2018. It is now read-only.

Threaded ICMP with tests, response_data, external runs with ruby 1.8.7, plus .... #30

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Gemfile.lock
*.gem
18 changes: 13 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,30 @@ before_install:
- gem install bundler
- bundle --version
bundler_args: --binstubs
script: bin/rake test
script: bin/rake test && ( [ -n "$EXCLUDE_ICMP_TEST" ] || (rvmsudo bin/rake test:icmp && rvmsudo ruby test/check_icmp_with_threads.rb ) )
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
- 2.0.0
- 2.1.0
- rbx
- 1.8.7
#- jruby-18mode
#- jruby-19mode
- jruby-head
- ruby-head
env:
- rvmsudo_secure_path=1
matrix:
include:
# https://jira.codehaus.org/browse/JRUBY-5897 - jruby doesn't support raw sockets
# jruby timeout doesn't work with udp sockets
# jruby: tcp tests failing with Errno::ECONNREFUSED to localhost port 22
- rvm: jruby-head
env: rvmsudo_secure_path=1 TESTOPTS=-v EXCLUDE_UDP_TEST=1 EXCLUDE_ICMP_TEST=1 EXCLUDE_TCP_TEST=1
- rvm: rbx
env: EXCLUDE_ICMP_TEST=1 rvmsudo_secure_path=1
allow_failures:
- rvm: 1.8.7
#- rvm: jruby-18mode
#- rvm: jruby-19mode
- rvm: rbx
- rvm: jruby-head
- rvm: ruby-head
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
source 'http://rubygems.org'
source 'https://rubygems.org'
gemspec
20 changes: 18 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,32 @@
reducing your memory footprint.

== Known Issues

**UDP**
Older versions of Ruby 1.9.x may not work with UDP pings.

Older versions of JRuby will return false positives in UDP pings
because of an incorrect error class being raised. See JRuby-4896.

Jruby hangs if UDP ping doesn't receive a return packet.
See http://jira.codehaus.org/browse/JRUBY-6974

**External**
JRuby 1.6.7 or later is required for external pings because of a bug
in earlier versions with open3 and stream handling.

ICMP pings are not thread safe. See https://www.ruby-forum.com/topic/146116.
Patches welcome.
Ruby versions 1.8.7 or earlier require a ping that returns "N% [packet ]loss"
since exitstatus isnt available from popoen3 call.

**ICMP**
ICMP pings are now thread safe as per https://www.ruby-forum.com/topic/146116.

ICMP ping only accepts IP numbers not hostnames.

ICMP pings are not supported by JRuby or Rubinus see https://jira.codehaus.org/browse/JRUBY-5897

**TCP**
TCP pings are not supported under JRuby (fail with Errno::ECONNREFUSED)

== License
Artistic 2.0
Expand Down
38 changes: 25 additions & 13 deletions lib/net/ping/external.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,32 @@ def ping(host = @host)

Timeout.timeout(@timeout){
Open3.popen3(*pcmd) do |stdin, stdout, stderr, thread|
stdin.close
err = stderr.gets # Can't chomp yet, might be nil

case thread.value.exitstatus
when 0
bool = true # Success, at least one response.
if err & err =~ /warning/i
@warning = err.chomp
end
when 2
bool = false # Transmission successful, no response.
@exception = err.chomp
else
bool = false # An error occurred
@exception = err.chomp
@response_data = stdout.read
if thread
case thread.value.exitstatus
when 0
bool = true # Success, at least one response.
if err & err =~ /warning/i
@warning = err.chomp
end
when 2
bool = false # Transmission successful, no response.
@exception = err.chomp
else
bool = false # An error occurred
@exception = err.chomp
end
elsif @response_data =~ /\D0+% (packet )?loss/
# Linux / windows loss percentage
bool = true
elsif @response_data =~ /\D(\d*[1-9]\d*% (packet )?loss)/
bool = false # An error occurred
@exception = "#{$1}"
else
bool = false # An error occurred
@exception = 'exitstatus could not be checked and stdout did not contain a recognisable loss percentage'
end
end
}
Expand Down
15 changes: 15 additions & 0 deletions lib/net/ping/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def ping(host = @host)
response = do_ping(uri, port)

if response.is_a?(Net::HTTPSuccess)
set_response_data(response)
bool = true
elsif redirect?(response) # Check code, HTTPRedirection does not always work
if @follow_redirect
Expand All @@ -107,6 +108,7 @@ def ping(host = @host)
end

if response.is_a?(Net::HTTPSuccess)
set_response_data(response)
bool = true
else
@warning = nil
Expand Down Expand Up @@ -167,5 +169,18 @@ def do_ping(uri, port)
@code = response.code if response
response
end

def set_response_data(response)
if @get_request
@response_data = response.body
else
@response_data = "HTTP#{response.http_version ? ('/' << response.http_version) : ''} #{response.code} #{response.message}\n"
response.header.each do |key, value|
@response_data << "#{key}: #{value}\n"
end
end
end

end

end
26 changes: 18 additions & 8 deletions lib/net/ping/icmp.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require File.join(File.dirname(__FILE__), 'ping')
require 'thread'

if File::ALT_SEPARATOR
require 'win32/security'
Expand Down Expand Up @@ -36,7 +37,6 @@ def initialize(host=nil, port=nil, timeout=5)
end
end

@seq = 0
@bind_port = 0
@bind_host = nil
@data_size = 56
Expand All @@ -50,6 +50,16 @@ def initialize(host=nil, port=nil, timeout=5)
@port = nil # This value is not used in ICMP pings.
end

# instance variables of the class, not available to sub classes
@seq = 0
@seq_mutex = Mutex.new

def self.next_seq
@seq_mutex.synchronize do
@seq = (@seq + 1) % 65536
end
end

# Sets the number of bytes sent in the ping method.
#
def data_size=(size)
Expand Down Expand Up @@ -85,15 +95,15 @@ def ping(host = @host)
socket.bind(saddr)
end

@seq = (@seq + 1) % 65536
seq = ICMP.next_seq
pstring = 'C2 n3 A' << @data_size.to_s
timeout = @timeout

checksum = 0
msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, @seq, @data].pack(pstring)
msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, seq, @data].pack(pstring)

checksum = checksum(msg)
msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, @seq, @data].pack(pstring)
msg = [ICMP_ECHO, ICMP_SUBCODE, checksum, @pid, seq, @data].pack(pstring)

begin
saddr = Socket.pack_sockaddr_in(0, host)
Expand All @@ -116,23 +126,23 @@ def ping(host = @host)
end

pid = nil
seq = nil
seq_received = nil

data = socket.recvfrom(1500).first
type = data[20, 2].unpack('C2').first

case type
when ICMP_ECHOREPLY
if data.length >= 28
pid, seq = data[24, 4].unpack('n3')
pid, seq_received = data[24, 4].unpack('n3')
end
else
if data.length > 56
pid, seq = data[52, 4].unpack('n3')
pid, seq_received = data[52, 4].unpack('n3')
end
end

if pid == @pid && seq == @seq && type == ICMP_ECHOREPLY
if pid == @pid && seq_received == seq && type == ICMP_ECHOREPLY
bool = true
break
end
Expand Down
10 changes: 10 additions & 0 deletions lib/net/ping/ping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ class Ping
#
attr_reader :duration

# The data to send to the remote host. Nil by default.
#
attr_accessor :data

# The data received from the remote host, if any.
#
attr_accessor :response_data

# The default constructor for the Net::Ping class. Accepts an optional
# +host+, +port+ and +timeout+. The port defaults to your echo port, or
# 7 if that happens to be undefined. The default timeout is 5 seconds.
Expand All @@ -61,6 +69,8 @@ def initialize(host=nil, port=nil, timeout=5)
@exception = nil
@warning = nil
@duration = nil
@data = nil
@response_data = nil

yield self if block_given?
end
Expand Down
21 changes: 20 additions & 1 deletion lib/net/ping/tcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def self.service_check=(bool)
@@service_check = bool
end

# Try and grab up to 1k from the first packet
MAX_DATA = 1024

# Creates and returns a new Ping::UDP object. This is effectively
# This method attempts to ping a host and port using a TCPSocket with
# the host, port and timeout values passed in the constructor. Returns
# true if successful, or false otherwise.
Expand Down Expand Up @@ -66,8 +70,20 @@ def ping(host=@host)
return false
end

resp = IO.select(nil, [sock], nil, timeout)
unless @data.nil? || @data.empty?
resp = IO.select(nil, [sock], nil, timeout)
if resp.nil?
if @@service_check
bool = true
else
@exception = Errno::ECONNREFUSED
end
return bool
end
sock.send(@data, 0)
end

resp = IO.select([sock], nil, nil, timeout)
# Assume ECONNREFUSED at this point
if resp.nil?
if @@service_check
Expand All @@ -77,6 +93,7 @@ def ping(host=@host)
end
else
bool = true
@response_data, ignore_addrinfo = sock.recvfrom(MAX_DATA)
end
ensure
sock.close if sock
Expand All @@ -100,3 +117,5 @@ class << self
end
end
end


17 changes: 7 additions & 10 deletions lib/net/ping/udp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Net

# The Ping::UDP class encapsulates methods for UDP pings.
class Ping::UDP < Ping
ECHO_PORT = 7

@@service_check = true

# Returns whether or not the connect behavior should enforce remote
Expand All @@ -31,19 +33,14 @@ def self.service_check=(bool)
# The maximum data size that can be sent in a UDP ping.
MAX_DATA = 64

# The data to send to the remote host. By default this is 'ping'.
# This should be MAX_DATA size characters or less.
#
attr_reader :data

# Creates and returns a new Ping::UDP object. This is effectively
# identical to its superclass constructor.
#
def initialize(host=nil, port=nil, timeout=5)
@data = 'ping'

super(host, port, timeout)

# Default data to 'ping' for UDP
@data = 'ping'
@bind_host = nil
@bind_port = nil
end
Expand Down Expand Up @@ -74,7 +71,6 @@ def bind(host, port)
#
def ping(host = @host)
super(host)

bool = false
udp = UDPSocket.open
array = []
Expand All @@ -89,7 +85,7 @@ def ping(host = @host)
Timeout.timeout(@timeout){
udp.connect(host, @port)
udp.send(@data, 0)
array = udp.recvfrom(MAX_DATA)
@response_data, array = udp.recvfrom(MAX_DATA)
}
rescue Errno::ECONNREFUSED, Errno::ECONNRESET => err
if @@service_check
Expand All @@ -100,7 +96,8 @@ def ping(host = @host)
rescue Exception => err
@exception = err
else
if array[0] == @data
# expect something back, but might not match data sent (echo port is a well known exception)
if @response_data && (@response_data == @data || @port != ECHO_PORT)
bool = true
end
ensure
Expand Down
3 changes: 2 additions & 1 deletion net-ping.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Gem::Specification.new do |spec|

spec.add_development_dependency('test-unit')
spec.add_development_dependency('fakeweb')
spec.add_development_dependency('rake')
# maintain ruby 1.8.7 compatibility
spec.add_development_dependency('rake', '< 10.2.0')

if File::ALT_SEPARATOR
require 'rbconfig'
Expand Down
Loading