ws_discovery 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/History.rdoc +3 -0
- data/README.rdoc +77 -0
- data/Rakefile +16 -0
- data/lib/ws_discovery/core_ext/socket_patch.rb +16 -0
- data/lib/ws_discovery/error.rb +4 -0
- data/lib/ws_discovery/multicast_connection.rb +77 -0
- data/lib/ws_discovery/network_constants.rb +13 -0
- data/lib/ws_discovery/response.rb +95 -0
- data/lib/ws_discovery/searcher.rb +91 -0
- data/lib/ws_discovery/version.rb +3 -0
- data/lib/ws_discovery.rb +61 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/application.css +1110 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/application.js +626 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/favicon_green.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/favicon_red.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/favicon_yellow.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/loading.gif +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/magnify.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/spec/ws_discovery/coverage/index.html +72 -0
- data/spec/ws_discovery/multicast_connection_spec.rb +108 -0
- data/spec/ws_discovery/response_spec.rb +118 -0
- data/spec/ws_discovery/searcher_spec.rb +85 -0
- data/spec/ws_discovery_spec.rb +43 -0
- data/ws_discovery.gemspec +32 -0
- metadata +296 -0
data/Gemfile
ADDED
data/History.rdoc
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
= ws_discovery
|
2
|
+
|
3
|
+
* {Homepage}[https://github.com/pelco-automation/ws-discovery]
|
4
|
+
* {WS-Discovery 1.0 Specification}[http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf]
|
5
|
+
* {SOAP-over-UDP Specification}[http://specs.xmlsoap.org/ws/2004/09/soap-over-udp/soap-over-udp.pdf]
|
6
|
+
|
7
|
+
== Description
|
8
|
+
|
9
|
+
This gem aims to provide the ability to search for WS-Discovery compatible target
|
10
|
+
services.
|
11
|
+
|
12
|
+
This uses EventMachine[http://github.com/eventmachine/eventmachine], so if
|
13
|
+
you're not already, getting familiar with its concepts will be helpful here.
|
14
|
+
|
15
|
+
== Features
|
16
|
+
|
17
|
+
* Search for WS-Discovery compatible target services.
|
18
|
+
|
19
|
+
== Examples
|
20
|
+
|
21
|
+
=== WS-Discovery Searches
|
22
|
+
|
23
|
+
A WS-Discovery search simply sends the probe out to the multicast group and
|
24
|
+
listens for responses for a given (or default of 5 seconds) amount of time. The
|
25
|
+
return from this depends on if you're running it within an EventMachine reactor
|
26
|
+
or not. If not, it returns an Array of responses as WSDiscovery::Responses.
|
27
|
+
Take a look at the WSDiscovery#search docs for more on the options here.
|
28
|
+
|
29
|
+
require 'ws_discovery'
|
30
|
+
|
31
|
+
# Search for all devices (do a probe with Types left unspecified)
|
32
|
+
all_devices = WSDiscovery.search # this is default
|
33
|
+
|
34
|
+
# Search for devices of a specific Type
|
35
|
+
network_video_transmitters = WSDiscovery.search(
|
36
|
+
env_namespaces: { "xmlns:dn" => "http://www.onvif.org/ver10/network/wsdl" },
|
37
|
+
types: "dn:NetworkVideoTransmitter")
|
38
|
+
|
39
|
+
# These searches will return an Array of WSDiscovery::Responses. See the
|
40
|
+
# WSDiscovery::Response documentation for more information.
|
41
|
+
|
42
|
+
If you do the search inside of an EventMachine reactor, as the
|
43
|
+
WSDiscovery::Searcher receives and parses responses, it adds them to the accessor
|
44
|
+
#discovery_responses, which is an EventMachine::Channel. This lets you subscribe
|
45
|
+
to the responses and do what you want with them.
|
46
|
+
|
47
|
+
== Requirements
|
48
|
+
|
49
|
+
* Ruby
|
50
|
+
* 1.9.3
|
51
|
+
* Gems
|
52
|
+
* builder
|
53
|
+
* eventmachine
|
54
|
+
* log_switch
|
55
|
+
* nokogiri
|
56
|
+
* nori
|
57
|
+
* uuid
|
58
|
+
* Gems (development)
|
59
|
+
* bundler
|
60
|
+
* rake
|
61
|
+
* rspec
|
62
|
+
* simplecov
|
63
|
+
* simplecov-rcov
|
64
|
+
* yard
|
65
|
+
|
66
|
+
== Install
|
67
|
+
|
68
|
+
$ gem install ws_discovery
|
69
|
+
|
70
|
+
== THANKS
|
71
|
+
|
72
|
+
The initial core of this gem came from https://github.com/turboladen/upnp due to
|
73
|
+
the similarities in how SSDP and WS-Discovery searches are performed.
|
74
|
+
|
75
|
+
The WSDiscovery::Response class reuses parts of https://github.com/savonrb/savon.
|
76
|
+
It made sense to me that WSDiscovery::Responses would behave similarly to
|
77
|
+
Savon::SOAP::Responses.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
YARD::Rake::YardocTask.new
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
9
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
10
|
+
spec.rcov = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task default: :install
|
14
|
+
|
15
|
+
# Alias for rubygems-test
|
16
|
+
task test: :spec
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
# Workaround for missing constants on Windows
|
4
|
+
module Socket::Constants
|
5
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
6
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
7
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
8
|
+
IP_TTL = 4 unless defined? IP_TTL
|
9
|
+
end
|
10
|
+
|
11
|
+
class Socket
|
12
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
13
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
14
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
15
|
+
IP_TTL = 4 unless defined? IP_TTL
|
16
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require_relative 'core_ext/socket_patch'
|
2
|
+
require_relative 'network_constants'
|
3
|
+
require_relative 'error'
|
4
|
+
require 'ipaddr'
|
5
|
+
require 'socket'
|
6
|
+
require 'eventmachine'
|
7
|
+
|
8
|
+
module WSDiscovery
|
9
|
+
class MulticastConnection < EventMachine::Connection
|
10
|
+
include WSDiscovery::NetworkConstants
|
11
|
+
|
12
|
+
# @param [Fixnum] ttl The TTL value to use when opening the UDP socket
|
13
|
+
# required for WSDiscovery actions.
|
14
|
+
def initialize ttl=TTL
|
15
|
+
@ttl = ttl
|
16
|
+
@discovery_responses = EM::Channel.new
|
17
|
+
|
18
|
+
setup_multicast_socket
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Gets the IP and port from the peer that just sent data.
|
24
|
+
#
|
25
|
+
# @return [Array<String,Fixnum>] The IP and port.
|
26
|
+
def peer_info
|
27
|
+
peer_bytes = get_peername[2, 6].unpack("nC4")
|
28
|
+
port = peer_bytes.first.to_i
|
29
|
+
ip = peer_bytes[1, 4].join(".")
|
30
|
+
|
31
|
+
[ip, port]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets Socket options to allow for multicasting. If ENV["RUBY_TESTING_ENV"]
|
35
|
+
# is equal to "testing", then it doesn't turn off multicast looping.
|
36
|
+
def setup_multicast_socket
|
37
|
+
set_membership(IPAddr.new(MULTICAST_IP).hton + IPAddr.new('0.0.0.0').hton)
|
38
|
+
set_multicast_ttl(@ttl)
|
39
|
+
set_ttl(@ttl)
|
40
|
+
|
41
|
+
unless ENV["RUBY_TESTING_ENV"] == "testing"
|
42
|
+
switch_multicast_loop :off
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [String] membership The network byte ordered String that represents
|
47
|
+
# the IP(s) that should join the membership group.
|
48
|
+
def set_membership(membership)
|
49
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [Fixnum] ttl TTL to set IP_MULTICAST_TTL to.
|
53
|
+
def set_multicast_ttl(ttl)
|
54
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, [ttl].pack('i'))
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [Fixnum] ttl TTL to set IP_TTL to.
|
58
|
+
def set_ttl(ttl)
|
59
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_TTL, [ttl].pack('i'))
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param [Symbol] on_off Turn on/off multicast looping. Supply :on or :off.
|
63
|
+
# @raise [WSDiscovery::Error] If invalid option is given.
|
64
|
+
def switch_multicast_loop(on_off)
|
65
|
+
hex_value = case on_off
|
66
|
+
when :on, "\001"
|
67
|
+
"\001"
|
68
|
+
when :off, "\000"
|
69
|
+
"\000"
|
70
|
+
else
|
71
|
+
raise WSDiscovery::Error, "Can't switch IP_MULTICAST_LOOP to '#{on_off}'"
|
72
|
+
end
|
73
|
+
|
74
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, hex_value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'nori'
|
3
|
+
require_relative 'error'
|
4
|
+
|
5
|
+
Nori.configure do |config|
|
6
|
+
config.strip_namespaces = true
|
7
|
+
config.convert_tags_to { |tag| tag.snakecase.to_sym }
|
8
|
+
end
|
9
|
+
|
10
|
+
module WSDiscovery
|
11
|
+
|
12
|
+
# Represents the probe response.
|
13
|
+
class Response
|
14
|
+
|
15
|
+
attr_accessor :response
|
16
|
+
|
17
|
+
# @param [String] response Text of the response to a WSDiscovery probe.
|
18
|
+
def initialize(response)
|
19
|
+
@response = response
|
20
|
+
end
|
21
|
+
|
22
|
+
# Shortcut accessor for the SOAP response body Hash.
|
23
|
+
#
|
24
|
+
# @param [Symbol] key The key to access in the body Hash.
|
25
|
+
# @return [Hash,String] The accessed value.
|
26
|
+
def [](key)
|
27
|
+
body[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the SOAP response header as a Hash.
|
31
|
+
#
|
32
|
+
# @return [Hash] SOAP response header.
|
33
|
+
# @raise [WSDiscovery::Error] If unable to parse response.
|
34
|
+
def header
|
35
|
+
unless hash.has_key? :envelope
|
36
|
+
raise WSDiscovery::Error, "Unable to parse response body '#{to_xml}'"
|
37
|
+
end
|
38
|
+
|
39
|
+
hash[:envelope][:header]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the SOAP response body as a Hash.
|
43
|
+
#
|
44
|
+
# @return [Hash] SOAP response body.
|
45
|
+
# @raise [WSDiscovery::Error] If unable to parse response.
|
46
|
+
def body
|
47
|
+
unless hash.has_key? :envelope
|
48
|
+
raise WSDiscovery::Error, "Unable to parse response body '#{to_xml}'"
|
49
|
+
end
|
50
|
+
|
51
|
+
hash[:envelope][:body]
|
52
|
+
end
|
53
|
+
|
54
|
+
alias to_hash body
|
55
|
+
|
56
|
+
# Returns the complete SOAP response XML without normalization.
|
57
|
+
#
|
58
|
+
# @return [Hash] Complete SOAP response Hash.
|
59
|
+
def hash
|
60
|
+
@hash ||= Nori.parse(to_xml)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the SOAP response XML.
|
64
|
+
#
|
65
|
+
# @return [String] Raw SOAP response XML.
|
66
|
+
def to_xml
|
67
|
+
response
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a Nokogiri::XML::Document for the SOAP response XML.
|
71
|
+
#
|
72
|
+
# @return [Nokogiri::XML::Document] Document for the SOAP response.
|
73
|
+
def doc
|
74
|
+
@doc ||= Nokogiri::XML(to_xml)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns an Array of Nokogiri::XML::Node objects retrieved with the given +path+.
|
78
|
+
# Automatically adds all of the document's namespaces unless a +namespaces+ hash is provided.
|
79
|
+
#
|
80
|
+
# @param [String] path XPath to search.
|
81
|
+
# @param [Hash<String>] namespaces Namespaces to append.
|
82
|
+
def xpath(path, namespaces = nil)
|
83
|
+
doc.xpath(path, namespaces || xml_namespaces)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# XML Namespaces from the Document.
|
89
|
+
#
|
90
|
+
# @return [Hash] Namespaces from the Document.
|
91
|
+
def xml_namespaces
|
92
|
+
@xml_namespaces ||= doc.collect_namespaces
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'multicast_connection'
|
2
|
+
require_relative 'response'
|
3
|
+
require 'builder'
|
4
|
+
require 'log_switch'
|
5
|
+
require 'uuid'
|
6
|
+
|
7
|
+
class WSDiscovery::Searcher < WSDiscovery::MulticastConnection
|
8
|
+
extend LogSwitch
|
9
|
+
self.logger.datetime_format = "%Y-%m-%d %H:%M:%S "
|
10
|
+
|
11
|
+
# @return [EventMachine::Channel] Provides subscribers with responses from
|
12
|
+
# their search request.
|
13
|
+
attr_reader :discovery_responses
|
14
|
+
|
15
|
+
# @param [Hash] options The options for the probe.
|
16
|
+
# @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
|
17
|
+
# @option options [Hash<String>] :type_attributes Type attributes.
|
18
|
+
# @option options [String] :types Types.
|
19
|
+
# @option options [Hash<String>] :scope_attributes Scope attributes.
|
20
|
+
# @option options [String] :scopes Scopes.
|
21
|
+
# @option options [Fixnum] :ttl TTL for the probe.
|
22
|
+
def initialize(options={})
|
23
|
+
options[:ttl] ||= TTL
|
24
|
+
|
25
|
+
@search = probe(options)
|
26
|
+
|
27
|
+
super options[:ttl]
|
28
|
+
end
|
29
|
+
|
30
|
+
# This is the callback called by EventMachine when it receives data on the
|
31
|
+
# socket that's been opened for this connection. In this case, the method
|
32
|
+
# parses the probe matches into WSDiscovery::Responses and adds them to the
|
33
|
+
# appropriate EventMachine::Channel (provided as accessor methods). This
|
34
|
+
# effectively means that in each Channel, you get a WSDiscovery::Response
|
35
|
+
# for each response that comes in on the socket.
|
36
|
+
#
|
37
|
+
# @param [String] response The data received on this connection's socket.
|
38
|
+
def receive_data(response)
|
39
|
+
ip, port = peer_info
|
40
|
+
WSDiscovery::Searcher.log "<#{self.class}> Response from #{ip}:#{port}:\n#{response}\n"
|
41
|
+
parsed_response = parse(response)
|
42
|
+
@discovery_responses << parsed_response
|
43
|
+
end
|
44
|
+
|
45
|
+
# Converts the headers to a set of key-value pairs.
|
46
|
+
#
|
47
|
+
# @param [String] data The data to convert.
|
48
|
+
# @return [WSDiscovery::Response] The converted data.
|
49
|
+
def parse(data)
|
50
|
+
WSDiscovery::Response.new(data)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sends the probe that was built during init. Logs what was sent if the
|
54
|
+
# send was successful.
|
55
|
+
def post_init
|
56
|
+
if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
|
57
|
+
WSDiscovery::Searcher.log("Sent datagram search:\n#{@search}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Probe for target services supporting WS-Discovery.
|
62
|
+
#
|
63
|
+
# @param [Hash] options The options for the probe.
|
64
|
+
# @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
|
65
|
+
# @option options [Hash<String>] :type_attributes Type attributes.
|
66
|
+
# @option options [String] :types Types.
|
67
|
+
# @option options [Hash<String>] :scope_attributes Scope attributes.
|
68
|
+
# @option options [String] :scopes Scopes.
|
69
|
+
# @return [String] Probe SOAP message.
|
70
|
+
def probe(options={})
|
71
|
+
namespaces = {
|
72
|
+
'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
|
73
|
+
'xmlns:d' => 'http://schemas.xmlsoap.org/ws/2005/04/discovery',
|
74
|
+
'xmlns:s' => 'http://www.w3.org/2003/05/soap-envelope'
|
75
|
+
}
|
76
|
+
namespaces.merge options[:env_namespaces] if options[:env_namespaces]
|
77
|
+
|
78
|
+
Builder::XmlMarkup.new.s(:Envelope, namespaces) do |xml|
|
79
|
+
xml.s(:Header) do |xml|
|
80
|
+
xml.a(:Action, 'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe')
|
81
|
+
xml.a(:MessageID, "uuid:#{UUID.generate}")
|
82
|
+
xml.a(:To, 'urn:schemas-xmlsoap-org:ws:2005:04:discovery')
|
83
|
+
end
|
84
|
+
|
85
|
+
xml.s(:Body) do |xml|
|
86
|
+
xml.d(:Types, options[:type_attributes], options[:types])
|
87
|
+
xml.d(:Scopes, options[:scope_attributes], options[:scopes])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/ws_discovery.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'ws_discovery/core_ext/socket_patch'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
require_relative 'ws_discovery/error'
|
5
|
+
require_relative 'ws_discovery/network_constants'
|
6
|
+
require_relative 'ws_discovery/searcher'
|
7
|
+
|
8
|
+
module WSDiscovery
|
9
|
+
include NetworkConstants
|
10
|
+
|
11
|
+
DEFAULT_WAIT_TIME = 5
|
12
|
+
|
13
|
+
# Opens a UDP socket on 0.0.0.0, on an ephemeral port, has WSDiscovery::Searcher
|
14
|
+
# build and send the search request, then receives the responses. The search
|
15
|
+
# will stop after +response_wait_time+.
|
16
|
+
#
|
17
|
+
# @param [Hash] options The options for the probe.
|
18
|
+
# @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
|
19
|
+
# @option options [Hash<String>] :type_attributes Type attributes.
|
20
|
+
# @option options [String] :types Types.
|
21
|
+
# @option options [Hash<String>] :scope_attributes Scope attributes.
|
22
|
+
# @option options [String] :scopes Scopes.
|
23
|
+
# @return [Array<WSDiscovery::Response>,WSDiscovery::Searcher] Returns an
|
24
|
+
# Array of probe responses. If the reactor is already running this will return
|
25
|
+
# a WSDiscovery::Searcher which will make its accessors available so you can
|
26
|
+
# get responses in real time.
|
27
|
+
def self.search(options={})
|
28
|
+
response_wait_time = options[:response_wait_time] || DEFAULT_WAIT_TIME
|
29
|
+
responses = []
|
30
|
+
|
31
|
+
multicast_searcher = proc do
|
32
|
+
EM.open_datagram_socket('0.0.0.0', 0, WSDiscovery::Searcher, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
if EM.reactor_running?
|
36
|
+
return multicast_searcher.call
|
37
|
+
else
|
38
|
+
EM.run do
|
39
|
+
ms = multicast_searcher.call
|
40
|
+
|
41
|
+
ms.discovery_responses.subscribe do |notification|
|
42
|
+
responses << notification
|
43
|
+
end
|
44
|
+
|
45
|
+
EM.add_timer(response_wait_time) { EM.stop }
|
46
|
+
trap_signals
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
responses.flatten
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Traps INT, TERM, and HUP signals and stops the reactor.
|
56
|
+
def self.trap_signals
|
57
|
+
trap('INT') { EM.stop }
|
58
|
+
trap('TERM') { EM.stop }
|
59
|
+
trap('HUP') { EM.stop } if RUBY_PLATFORM !~ /mswin|mingw/
|
60
|
+
end
|
61
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'simplecov-rcov'
|
3
|
+
|
4
|
+
class SimpleCov::Formatter::MergedFormatter
|
5
|
+
def format(result)
|
6
|
+
SimpleCov::Formatter::HTMLFormatter.new.format(result)
|
7
|
+
SimpleCov::Formatter::RcovFormatter.new.format(result)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
|
12
|
+
|
13
|
+
SimpleCov.start do
|
14
|
+
add_filter "/spec"
|
15
|
+
add_filter "/lib/deps"
|
16
|
+
end
|
17
|
+
|
18
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
19
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
20
|
+
require 'rspec'
|
21
|
+
|
22
|
+
# Requires supporting files with custom matchers and macros, etc,
|
23
|
+
# in ./support/ and its subdirectories.
|
24
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
|
25
|
+
|
26
|
+
ENV["RUBY_TESTING_ENV"] = "testing"
|