dnssd 1.1.0 → 1.2

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.
@@ -1,153 +1,182 @@
1
1
  require 'dnssd/dnssd'
2
+ require 'socket'
2
3
 
3
4
  ##
4
- # DNSSD is a wrapper for Apple's DNS Service Discovery library. The methods
5
- # DNSSD.enumerate_domains, DNSSD.browse(), DNSSD.register(), and
6
- # DNSSD.resolve() provide the basic API for making your applications DNS
7
- # Service Discovery aware.
5
+ # DNSSD is a wrapper for the DNS Service Discovery library.
6
+ #
7
+ # DNSSD.announce and DNSSD::Reply.connect provide an easy-to-use way to
8
+ # announce and connect to services.
9
+ #
10
+ # The methods DNSSD.enumerate_domains, DNSSD.browse, DNSSD.register, and
11
+ # DNSSD.resolve provide the basic API for making your applications DNS \Service
12
+ # Discovery aware.
8
13
 
9
14
  module DNSSD
10
- VERSION = '1.1.0'
11
- end
12
15
 
13
- require 'dnssd/flags'
14
- require 'dnssd/reply'
15
- require 'dnssd/service'
16
- require 'dnssd/text_record'
16
+ ##
17
+ # The version of DNSSD you're using.
17
18
 
18
- =begin
19
+ VERSION = '1.2'
19
20
 
20
- module DNSSD
21
- class MalformedDomain < Error; end
22
- class MalformedPort < Error; end
21
+ ##
22
+ # Registers +socket+ with DNSSD as +name+. If +service+ is omitted it is
23
+ # looked up using #getservbyport and the ports address. +text_record+,
24
+ # +flags+ and +interface+ are used as in #register.
25
+ #
26
+ # Returns the Service created by registering the socket. The Service will
27
+ # automatically be shut down when #close or #close_read is called on the
28
+ # socket.
29
+ #
30
+ # Only for bound TCP and UDP sockets.
23
31
 
24
- def self.new_text_record(hash={})
25
- TextRecord.new(hash)
26
- end
32
+ def self.announce(socket, name, service = nil, text_record = nil, flags = 0,
33
+ interface = DNSSD::InterfaceAny, &block)
34
+ _, port, _, address = socket.addr
27
35
 
28
- class ServiceDescription
36
+ raise ArgumentError, 'socket not bound' if port == 0
29
37
 
30
- class Location
31
- attr_accessor :name, :port, :iface
32
- def initialize(port, name=nil, iface=0)
33
- @name = name
34
- @port = port
35
- @iface = iface
36
- end
37
- end
38
+ service ||= DNSSD.getservbyport port
38
39
 
39
- def initialize(type, name, domain, location)
40
- @type = type
41
- @name = name
42
- @domain = validate_domain(domain)
43
- @location = validate_location(location)
44
- end
40
+ proto = case socket
41
+ when TCPSocket then 'tcp'
42
+ when UDPSocket then 'udp'
43
+ else raise ArgumentError, 'tcp or udp sockets only'
44
+ end
45
45
 
46
- def self.for_browse_notification(name, domain,
46
+ type = "_#{service}._#{proto}"
47
47
 
48
- def stop
48
+ registrar = register(name, type, nil, port, text_record, flags, interface,
49
+ &block)
50
+
51
+ socket.instance_variable_set :@registrar, registrar
52
+
53
+ def socket.close
54
+ result = super
49
55
  @registrar.stop
56
+ return result
50
57
  end
51
58
 
52
- def validate_location(location)
53
- unless(location.port.respond_to?(:to_int))
54
- raise MalformedPort.new("#{location.port} is not a valid port number")
55
- end
56
- location
59
+ def socket.close_read
60
+ result = super
61
+ @registrar.stop
62
+ return result
57
63
  end
58
64
 
59
- def validate_domain(domain)
60
- unless(domain.empty? || domain =~ /^[a-z_]+$/)
61
- raise MalformedDomain.new("#{domain} is not a valid domain name")
62
- end
63
- domain
64
- end
65
+ registrar
66
+ end
65
67
 
66
- def advertise_and_confirm
67
- thread = Thread.current
68
- @registrar = register(@name, @type, @domain, @location.port, TextRecord.new) do |service, name, type, domain|
69
- @name = name
70
- @type = type
71
- @domain = domain
72
- thread.wakeup
73
- end
74
- Thread.stop
75
- end
68
+ ##
69
+ # Asynchronous version of DNSSD::Service#browse
76
70
 
77
- def self.advertise_http(name, port=80, domain="", iface=0, &block)
78
- self.advertise("_http._tcp", name, port, domain, iface, &block)
71
+ def self.browse(type, domain = nil, flags = 0,
72
+ interface = DNSSD::InterfaceAny, &block)
73
+ service = DNSSD::Service.new
74
+
75
+ Thread.start do
76
+ run(service, :browse, type, domain, flags, interface, &block)
79
77
  end
80
78
 
81
- ##
82
- # iface: Numerical interface (0 = all interfaces, This should be used for
83
- # most applications)
79
+ service
80
+ end
81
+
82
+ ##
83
+ # Synchronous version of DNSSD::Service#browse
84
84
 
85
- def self.advertise(type, name, port, domain="", iface=0, &block)
86
- service_description = ServiceDescription.new(type, name, domain, Location.new(port,nil,iface))
87
- service_description.advertise_and_confirm
88
- yield service_description if block_given?
89
- service_description
85
+ def self.browse!(type, domain = nil, flags = 0,
86
+ interface = DNSSD::InterfaceAny, &block)
87
+ service = DNSSD::Service.new
88
+
89
+ run(service, :browse, type, domain, flags, interface, &block)
90
+ end
91
+
92
+ ##
93
+ # Asynchronous version of DNSSD::Service#enumerate_domains
94
+
95
+ def self.enumerate_domains(flags = DNSSD::Flags::BrowseDomains,
96
+ interface = DNSSD::InterfaceAny, &block)
97
+ service = DNSSD::Service.new
98
+
99
+ Thread.start do
100
+ run(service, :enumerate_domains, flags, interface, &block)
90
101
  end
102
+
103
+ service
91
104
  end
92
105
 
93
- class Browser
106
+ ##
107
+ # Synchronous version of DNSSD::Service#enumerate_domains
94
108
 
95
- Context = Struct.new(:service, :name, :type, :domain, :operation, :interface)
109
+ def self.enumerate_domains!(flags = DNSSD::Flags::BrowseDomains,
110
+ interface = DNSSD::InterfaceAny, &block)
111
+ service = DNSSD::Service.new
96
112
 
97
- class Context
98
- def ==(other)
99
- self.to_s == other.to_s
100
- end
113
+ run(service, :enumerate_domains, flags, interface, &block)
114
+ end
101
115
 
102
- def to_s
103
- "#{name}.#{type}.#{domain}"
104
- end
116
+ ##
117
+ # Asynchronous version of DNSSD::Service#register
105
118
 
106
- def eql?(other)
107
- self == other
108
- end
119
+ def self.register(name, type, domain, port, text_record = nil, flags = 0,
120
+ interface = DNSSD::InterfaceAny, &block)
121
+ service = DNSSD::Service.new
109
122
 
110
- def hash
111
- to_s.hash
112
- end
123
+ Thread.start do
124
+ run(service, :register, name, type, domain, port, nil, text_record,
125
+ flags, interface, &block)
113
126
  end
114
127
 
115
- def on_change(&block)
116
- @change_listener ||= []
117
- @change_listeners << block
118
- end
128
+ service
129
+ end
119
130
 
120
- def on_add(&block)
121
- @add_listeners || = []
122
- @add_listeners << block
123
- end
131
+ ##
132
+ # Synchronous version of DNSSD::Service#register
124
133
 
125
- def on_remove(&block)
126
- @remove_listeners || = []
127
- @remove_listeners << block
128
- end
134
+ def self.register!(name, type, domain, port, text_record = nil, flags = 0,
135
+ interface = DNSSD::InterfaceAny, &block)
136
+ service = DNSSD::Service.new
129
137
 
130
- def initialize(type, domain="")
131
- @list = []
132
- @browse_service = DNSSD::Protocol.browse(type, domain) do
133
- |service, name, type, domain, operation, interface|
134
- context = Context.new(service, name, type, domain, operation, interface)
135
- puts "Name: #{name} Type: #{type} Domain: #{domain} Operation: #{operation} Interface: #{interface}"
136
- end
137
- end
138
+ run(service, :register, name, type, domain, port, nil, text_record, flags,
139
+ interface, &block)
140
+ end
138
141
 
139
- def service_descriptions
140
- @list.clone
141
- end
142
+ ##
143
+ # Asynchronous version of DNSSD::Service#resolve
142
144
 
143
- def stop
144
- @browse_service.stop
145
- end
145
+ def self.resolve(*args, &block)
146
+ service = DNSSD::Service.new
146
147
 
147
- def self.for_http(domain="")
148
- self.new("_http._tcp", domain)
148
+ Thread.start do
149
+ run(service, :resolve, *args, &block)
149
150
  end
151
+
152
+ service
153
+ end
154
+
155
+ ##
156
+ # Synchronous version of DNSSD::Service#resolve
157
+
158
+ def self.resolve!(*args, &block)
159
+ service = DNSSD::Service.new
160
+
161
+ run(service, :resolve, *args, &block)
162
+ end
163
+
164
+ ##
165
+ # Dispatches +args+ and +block+ to +method+ on +service+ and ensures
166
+ # +service+ is shut down after use.
167
+
168
+ def self.run(service, method, *args, &block)
169
+ service.send(method, *args, &block)
170
+
171
+ service
172
+ ensure
173
+ service.stop unless service.stopped?
150
174
  end
175
+
151
176
  end
152
177
 
153
- =end
178
+ require 'dnssd/flags'
179
+ require 'dnssd/reply'
180
+ require 'dnssd/service'
181
+ require 'dnssd/text_record'
182
+
@@ -22,6 +22,9 @@ class DNSSD::Flags
22
22
  end
23
23
  end
24
24
 
25
+ ##
26
+ # Bitfield with all valid flags set
27
+
25
28
  ALL_FLAGS = FLAGS.values.inject { |flag, all| flag | all }
26
29
 
27
30
  ##
@@ -71,16 +74,25 @@ class DNSSD::Flags
71
74
  verify
72
75
  end
73
76
 
77
+ ##
78
+ # Returns an Array of flag names
79
+
74
80
  def to_a
75
81
  FLAGS.map do |name, value|
76
82
  (@flags & value == value) ? name : nil
77
83
  end.compact
78
84
  end
79
85
 
80
- def to_i # :nodoc:
86
+ ##
87
+ # Flags as a bitfield
88
+
89
+ def to_i
81
90
  @flags
82
91
  end
83
92
 
93
+ ##
94
+ # Trims the flag list down to valid flags
95
+
84
96
  def verify
85
97
  @flags &= ALL_FLAGS
86
98
 
@@ -29,7 +29,7 @@ class DNSSD::Reply
29
29
  attr_reader :port
30
30
 
31
31
  ##
32
- # The service associated with the reply, see DNSSD::Service
32
+ # The DNSSD::Service associated with the reply
33
33
 
34
34
  attr_reader :service
35
35
 
@@ -48,6 +48,9 @@ class DNSSD::Reply
48
48
 
49
49
  attr_reader :type
50
50
 
51
+ ##
52
+ # Creates a DNSSD::Reply from +service+ and +flags+
53
+
51
54
  def self.from_service(service, flags)
52
55
  reply = new
53
56
  reply.instance_variable_set :@service, service
@@ -55,6 +58,52 @@ class DNSSD::Reply
55
58
  reply
56
59
  end
57
60
 
61
+ ##
62
+ # Connects to this Reply. If +target+ and +port+ are missing, DNSSD.resolve
63
+ # is automatically called. +family+ can be used to select a particular
64
+ # address family.
65
+
66
+ def connect(family = Socket::AF_UNSPEC)
67
+ unless target and port then
68
+ value = nil
69
+
70
+ DNSSD.resolve! self do |reply|
71
+ value = reply
72
+ break
73
+ end
74
+
75
+ return value.connect
76
+ end
77
+
78
+ socktype = case protocol
79
+ when 'tcp' then Socket::SOCK_STREAM
80
+ when 'udp' then Socket::SOCK_DGRAM
81
+ else raise ArgumentError, "invalid protocol #{protocol}"
82
+ end
83
+
84
+ addresses = Socket.getaddrinfo target, port, family, socktype
85
+
86
+ socket = nil
87
+
88
+ addresses.each do |address|
89
+ begin
90
+ case protocol
91
+ when 'tcp' then
92
+ socket = TCPSocket.new address[3], port
93
+ when 'udp' then
94
+ socket = UDPSocket.new
95
+ socket.connect address[3], port rescue next
96
+ end
97
+
98
+ return socket
99
+ rescue
100
+ next
101
+ end
102
+ end
103
+
104
+ raise DNSSD::Error, "unable to connect to #{target}:#{port}" unless socket
105
+ end
106
+
58
107
  ##
59
108
  # The full service domain name, see DNSS::Service#fullname
60
109
 
@@ -62,12 +111,29 @@ class DNSSD::Reply
62
111
  DNSSD::Service.fullname @name.gsub("\032", ' '), @type, @domain
63
112
  end
64
113
 
65
- def inspect
114
+ def inspect # :nodoc:
66
115
  "#<%s:0x%x %p type: %s domain: %s interface: %s flags: %s>" % [
67
116
  self.class, object_id, @name, @type, @domain, @interface, @flags
68
117
  ]
69
118
  end
70
119
 
120
+ ##
121
+ # Protocol of this service
122
+
123
+ def protocol
124
+ type.split('.').last.sub '_', ''
125
+ end
126
+
127
+ ##
128
+ # Service name as in Socket.getservbyname
129
+
130
+ def service_name
131
+ type.split('.').first.sub '_', ''
132
+ end
133
+
134
+ ##
135
+ # Sets #name, #type and #domain from +fullname+
136
+
71
137
  def set_fullname(fullname)
72
138
  fullname = fullname.gsub(/\\([0-9]+)/) do $1.to_i.chr end
73
139
  fullname = fullname.scan(/(?:[^\\.]|\\\.)+/).map do |part|
@@ -79,6 +145,9 @@ class DNSSD::Reply
79
145
  @domain = fullname.last + '.'
80
146
  end
81
147
 
148
+ ##
149
+ # Sets #name, #type and #domain
150
+
82
151
  def set_names(name, type, domain)
83
152
  set_fullname [name, type, domain].join('.')
84
153
  end
@@ -1,15 +1,158 @@
1
+ require 'thread'
2
+
3
+ ##
4
+ # A DNSSD::Service may be used for one DNS-SD call at a time. Between calls
5
+ # the service must be stopped. A single service can be reused multiple times.
6
+ #
7
+ # DNSSD::Service provides the raw DNS-SD functions via the _ variants.
8
+
1
9
  class DNSSD::Service
2
10
 
3
11
  ##
4
- # Access the services underlying thread. Returns nil if the service is
5
- # synchronous.
12
+ # Creates a new DNSSD::Service
13
+
14
+ def initialize
15
+ @replies = []
16
+ @continue = true
17
+ @thread = nil
18
+ end
19
+
20
+ ##
21
+ # Browse for services.
22
+ #
23
+ # For each service found a DNSSD::Reply object is yielded.
24
+ #
25
+ # service = DNSSD::Service.new
26
+ # timeout 6 do
27
+ # service.browse '_http._tcp' do |r|
28
+ # puts "Found HTTP service: #{r.name}"
29
+ # end
30
+ # rescue Timeout::Error
31
+ # end
32
+
33
+ def browse(type, domain = nil, flags = 0, interface = DNSSD::InterfaceAny,
34
+ &block)
35
+ check_domain domain
36
+ interface = DNSSD.interface_index interface unless Integer === interface
37
+
38
+ raise DNSSD::Error, 'service in progress' if started?
39
+
40
+ _browse type, domain, flags.to_i, interface
41
+
42
+ process(&block)
43
+ end
6
44
 
7
- attr_reader :thread
45
+ ##
46
+ # Raises an ArgumentError if +domain+ is too long including NULL terminator
47
+ # and trailing '.'
48
+
49
+ def check_domain(domain)
50
+ return unless domain
51
+ raise ArgumentError, 'domain name string is too long' if
52
+ domain.length >= MAX_DOMAIN_NAME - 1
53
+ end
54
+
55
+ ##
56
+ # Enumerate domains available for browsing and registration.
57
+ #
58
+ # For each domain found a DNSSD::Reply object is passed to block with
59
+ # #domain set to the enumerated domain.
60
+ #
61
+ # available_domains = []
62
+ #
63
+ # timeout(2) do
64
+ # DNSSD.enumerate_domains! do |r|
65
+ # available_domains << r.domain
66
+ # end
67
+ # rescue TimeoutError
68
+ # end
69
+ #
70
+ # p available_domains
71
+
72
+ def enumerate_domains(flags = DNSSD::Flags::BrowseDomains,
73
+ interface = DNSSD::InterfaceAny, &block)
74
+ interface = DNSSD.interface_index interface unless Integer === interface
75
+
76
+ raise DNSSD::Error, 'service in progress' if started?
77
+
78
+ _enumerate_domains flags.to_i, interface
79
+
80
+ process(&block)
81
+ end
8
82
 
9
- def inspect
83
+ def inspect # :nodoc:
10
84
  stopped = stopped? ? 'stopped' : 'running'
11
85
  "#<%s:0x%x %s>" % [self.class, object_id, stopped]
12
86
  end
13
87
 
88
+ def process
89
+ @thread = Thread.current
90
+
91
+ while @continue do
92
+ _process if @replies.empty?
93
+ yield @replies.shift until @replies.empty?
94
+ end
95
+
96
+ @thread = nil
97
+
98
+ self
99
+ end
100
+
101
+ ##
102
+ # Register a service. A DNSSD::Reply object is passed to the optional block
103
+ # when the registration completes.
104
+ #
105
+ # DNSSD.register! "My Files", "_http._tcp", nil, 8080 do |r|
106
+ # puts "successfully registered: #{r.inspect}"
107
+ # end
108
+
109
+ def register(name, type, domain, port, host = nil, text_record = nil,
110
+ flags = 0, interface = DNSSD::InterfaceAny, &block)
111
+ check_domain domain
112
+ interface = DNSSD.interface_index interface unless Integer === interface
113
+
114
+ raise DNSSD::Error, 'service in progress' if started?
115
+
116
+ _register name, type, domain, host, port, text_record, flags.to_i, interface
117
+
118
+ block = proc { } unless block
119
+
120
+ process(&block)
121
+ end
122
+
123
+ ##
124
+ # Resolve a service discovered via #browse.
125
+ #
126
+ # +name+ may be either the name of the service found or a DNSSD::Reply from
127
+ # DNSSD::Service#browse. When +name+ is a DNSSD::Reply, +type+ and +domain+
128
+ # are automatically filled in, otherwise the service type and domain must be
129
+ # supplied.
130
+ #
131
+ # The service is resolved to a target host name, port number, and text
132
+ # record, all contained in the DNSSD::Reply object passed to the required
133
+ # block.
134
+ #
135
+ # The returned service can be used to control when to stop resolving the
136
+ # service (see DNSSD::Service#stop).
137
+ #
138
+ # s = DNSSD.resolve "foo bar", "_http._tcp", "local" do |r|
139
+ # p r
140
+ # end
141
+ # sleep 2
142
+ # s.stop
143
+
144
+ def resolve(name, type = name.type, domain = name.domain, flags = 0,
145
+ interface = DNSSD::InterfaceAny, &block)
146
+ name = name.name if DNSSD::Reply === name
147
+ check_domain domain
148
+ interface = DNSSD.interface_index interface unless Integer === interface
149
+
150
+ raise DNSSD::Error, 'service in progress' if started?
151
+
152
+ _resolve name, type, domain, flags.to_i, interface
153
+
154
+ process(&block)
155
+ end
156
+
14
157
  end
15
158