dnssd 1.1.0 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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