UPnP 1.0.0

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.
Binary file
@@ -0,0 +1,30 @@
1
+ # vim: filetype=ruby
2
+
3
+ Autotest.add_hook :initialize do |at|
4
+
5
+ at.add_mapping %r%^lib/UPnP/SSDP.rb$% do
6
+ ['test/test_UPnP_SSDP.rb',
7
+ 'test/test_UPnP_SSDP_notification.rb',
8
+ 'test/test_UPnP_SSDP_response.rb',
9
+ ]
10
+ end
11
+
12
+ at.add_mapping %r%^lib/UPnP/control/(\w+).rb$% do |_,m|
13
+ "test/test_UPnP_Control_#{m[1].capitalize}.rb"
14
+ end
15
+
16
+ at.add_mapping %r%^test/utilities.rb$% do
17
+ at.known_files
18
+ end
19
+
20
+ at.extra_class_map["TestUPnPControlDevice"] =
21
+ 'test/test_UPnP_control_device.rb'
22
+ at.extra_class_map["TestUPnPSSDP"] = 'test/test_UPnP_SSDP.rb'
23
+ at.extra_class_map["TestUPnPSSDPNotification"] =
24
+ 'test/test_UPnP_SSDP_notification.rb'
25
+ at.extra_class_map["TestUPnPSSDPResponse"] =
26
+ 'test/test_UPnP_SSDP_response.rb'
27
+ at.extra_class_map["TestUPnPControlService"] =
28
+ 'test/test_UPnP_control_service.rb'
29
+ end
30
+
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-06-25
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,18 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/upnp_discover
7
+ bin/upnp_listen
8
+ lib/UPnP.rb
9
+ lib/UPnP/SSDP.rb
10
+ lib/UPnP/control.rb
11
+ lib/UPnP/control/device.rb
12
+ lib/UPnP/control/service.rb
13
+ test/test_UPnP_SSDP.rb
14
+ test/test_UPnP_SSDP_notification.rb
15
+ test/test_UPnP_SSDP_response.rb
16
+ test/test_UPnP_control_device.rb
17
+ test/test_UPnP_control_service.rb
18
+ test/utilities.rb
@@ -0,0 +1,95 @@
1
+ = UPnP
2
+
3
+ * http://seattlerb.org/UPnP
4
+ * http://upnp.org
5
+ * Bugs: http://rubyforge.org/tracker/?atid=5921&group_id=1513
6
+
7
+ == DESCRIPTION:
8
+
9
+ An implementation of the UPnP protocol
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ * Discovers UPnP devices and services via SSDP, see UPnP::SSDP
14
+ * Creates a SOAP RPC driver for discovered services, see UPnP::Control::Service
15
+ * Creates concrete UPnP device and service classes that may be extended with
16
+ utility methods, see UPnP::Control::Device::create,
17
+ UPnP::Control::Service::create and the UPnP-IGD gem.
18
+ * Eventing not implemented
19
+ * Servers not implemented
20
+
21
+ == SYNOPSIS:
22
+
23
+ Print out information about UPnP devices nearby:
24
+
25
+ upnp_discover
26
+
27
+ Listen for UPnP resource notifications:
28
+
29
+ upnp_listen
30
+
31
+ Search for root UPnP devices and print out their description URLs:
32
+
33
+ require 'UPnP/SSDP'
34
+
35
+ resources = UPnP::SSDP.new.search :root
36
+ locations = resources.map { |resource| resource.location }
37
+ puts locations.join("\n")
38
+
39
+ Create a UPnP::Control::Device from the first discovered root device:
40
+
41
+ require 'UPnP/control/device'
42
+
43
+ device = UPnP::Control::Device.create locations.first
44
+
45
+ Enumerate actions on all services on the device:
46
+
47
+ service_names = device.services.map do |service|
48
+ service.methods(false)
49
+ end
50
+
51
+ puts service_names.sort.join("\n")
52
+
53
+ Assuming the root device is an InternetGatewayDevice with a WANIPConnection
54
+ service, print out the external IP address for the gateway:
55
+
56
+ wic = device.services.find { |service| service.type =~ /WANIPConnection/ }
57
+ puts wic.GetExternalIPAddress
58
+
59
+ == REQUIREMENTS:
60
+
61
+ * UPnP devices
62
+
63
+ == INSTALL:
64
+
65
+ sudo gem install UPnP
66
+
67
+ == LICENSE:
68
+
69
+ All code copyright 2008 Eric Hodel. All rights reserved.
70
+
71
+ Redistribution and use in source and binary forms, with or without
72
+ modification, are permitted provided that the following conditions
73
+ are met:
74
+
75
+ 1. Redistributions of source code must retain the above copyright
76
+ notice, this list of conditions and the following disclaimer.
77
+ 2. Redistributions in binary form must reproduce the above copyright
78
+ notice, this list of conditions and the following disclaimer in the
79
+ documentation and/or other materials provided with the distribution.
80
+ 3. Neither the names of the authors nor the names of their contributors
81
+ may be used to endorse or promote products derived from this software
82
+ without specific prior written permission.
83
+
84
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
85
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
86
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
87
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
88
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
89
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
90
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
91
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
92
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
93
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
94
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
95
+
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/upnp.rb'
6
+
7
+ Hoe.new('UPnP', UPnP::VERSION) do |p|
8
+ p.rubyforge_name = 'seattlerb'
9
+ p.developer('Eric Hodel', 'drbrain@segment7.net')
10
+ end
11
+
12
+ # vim: syntax=Ruby
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'UPnP/SSDP'
4
+ require 'UPnP/control'
5
+
6
+ ssdp = UPnP::SSDP.new
7
+ timeout = ARGV.shift
8
+ timeout = if timeout then
9
+ begin
10
+ timeout = Integer timeout
11
+ rescue
12
+ abort <<-EOF
13
+ Usage: #{$0} [timeout]
14
+
15
+ Prints information about UPnP internet gateway devices
16
+ EOF
17
+ end
18
+ else
19
+ 1
20
+ end
21
+
22
+ ssdp.timeout = timeout
23
+
24
+ devices = ssdp.search(:root).map do |resource|
25
+ UPnP::Control::Device.new resource.location
26
+ end
27
+
28
+ if devices.empty? then
29
+ puts 'No UPnP devices found'
30
+ exit
31
+ end
32
+
33
+ def print_device(device, indent = ' ')
34
+ out = []
35
+
36
+ out << "Friendly name: #{device.friendly_name}"
37
+ out << "Presentation URL: #{device.presentation_url}"
38
+ out << nil
39
+ out << "Unique device name: #{device.name}"
40
+ out << nil
41
+ out << "Manufacturer: #{device.manufacturer}"
42
+ out << "Manufacturer URL: #{device.manufacturer_url}"
43
+ out << nil
44
+ out << "Model name: #{device.model_name}"
45
+ out << "Model description: #{device.model_description}"
46
+ out << "Model URL: #{device.model_url}"
47
+ out << "UPC: #{device.upc}"
48
+ out << "Serial number: #{device.serial_number}"
49
+ out << nil
50
+
51
+ puts indent + out.join("\n#{indent}")
52
+ end
53
+
54
+ devices.each do |device|
55
+ type = device.type.sub "#{UPnP::DEVICE_SCHEMA_PREFIX}:", ''
56
+ puts "#{device.url}: #{type}"
57
+ print_device device
58
+
59
+ device.devices.each do |sub_device|
60
+ type = sub_device.type.sub "#{UPnP::DEVICE_SCHEMA_PREFIX}:", ''
61
+ puts " Sub-device #{type}:"
62
+ print_device sub_device, ' '
63
+ end
64
+
65
+ device.services.each do |service|
66
+ type = service.type.sub("#{UPnP::DEVICE_SCHEMA_PREFIX}:", '')
67
+ puts " Service: #{type}"
68
+ puts " Id: #{service.id}"
69
+ puts " Type: #{service.type}"
70
+ puts " SCPD URL: #{service.scpd_url}"
71
+ puts " Control URL: #{service.control_url}"
72
+ puts " Event subscription URL: #{service.event_sub_url}"
73
+ puts
74
+ puts " Actions:"
75
+ service.driver.methods(false).sort.each do |method|
76
+ puts " #{method}"
77
+ end
78
+
79
+ puts
80
+ end
81
+ end
82
+
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'UPnP/SSDP'
4
+ require 'UPnP/control'
5
+
6
+ ssdp = UPnP::SSDP.new
7
+
8
+ ssdp.discover do |notification|
9
+ schemas = Regexp.union UPnP::DEVICE_SCHEMA_PREFIX, UPnP::SERVICE_SCHEMA_PREFIX
10
+
11
+ type = notification.type.sub(/#{schemas}:/, '')
12
+
13
+ if notification.alive? then
14
+ puts "#{type} is alive"
15
+ puts "Description: #{notification.location}"
16
+ expiration = notification.expiration.strftime '%c'
17
+ puts "Valid until #{expiration}"
18
+ else
19
+ puts "#{type} says byebye"
20
+ end
21
+
22
+ puts "USN: #{notification.name}"
23
+
24
+ puts
25
+ end
26
+
@@ -0,0 +1,35 @@
1
+ $KCODE = 'u'
2
+
3
+ require 'rubygems'
4
+ gem 'soap4r'
5
+
6
+ ##
7
+ # An implementation of the Universal Plug and Play protocol.
8
+ #
9
+ # http://upnp.org/
10
+
11
+ module UPnP
12
+
13
+ ##
14
+ # UPnP device schema prefix
15
+
16
+ DEVICE_SCHEMA_PREFIX = 'urn:schemas-upnp-org:device'
17
+
18
+ ##
19
+ # UPnP service schema prefix
20
+
21
+ SERVICE_SCHEMA_PREFIX = 'urn:schemas-upnp-org:service'
22
+
23
+ ##
24
+ # The version of UPnP you are using
25
+
26
+ VERSION = '1.0.0'
27
+
28
+ ##
29
+ # UPnP error base class
30
+
31
+ class Error < RuntimeError
32
+ end
33
+
34
+ end
35
+
@@ -0,0 +1,495 @@
1
+ require 'ipaddr'
2
+ require 'socket'
3
+ require 'thread'
4
+ require 'time'
5
+ require 'uri'
6
+
7
+ require 'UPnP'
8
+ require 'UPnP/control'
9
+
10
+ ##
11
+ # Simple Service Discovery Protocol for the UPnP Device Architecture.
12
+ #
13
+ # Currently SSDP only handles the discovery portions of SSDP.
14
+ #
15
+ # To listen for SSDP notifications from UPnP devices:
16
+ #
17
+ # ssdp = SSDP.new
18
+ # notifications = ssdp.listen
19
+ #
20
+ # To discover all devices and services:
21
+ #
22
+ # ssdp = SSDP.new
23
+ # resources = ssdp.search
24
+ #
25
+ # After a device has been found you can create a Device object for it:
26
+ #
27
+ # UPnP::Control::Device.create resource.location
28
+ #
29
+ # Based on code by Kazuhiro NISHIYAMA (zn@mbf.nifty.com)
30
+
31
+ class UPnP::SSDP
32
+
33
+ ##
34
+ # SSDP Error class
35
+
36
+ class Error < UPnP::Error
37
+ end
38
+
39
+ ##
40
+ # Abstract class for SSDP advertisements
41
+
42
+ class Advertisement
43
+
44
+ ##
45
+ # Expiration time of this advertisement
46
+
47
+ def expiration
48
+ date + max_age
49
+ end
50
+
51
+ ##
52
+ # True if this advertisement has expired
53
+
54
+ def expired?
55
+ Time.now > expiration
56
+ end
57
+
58
+ end
59
+
60
+ ##
61
+ # Holds information about a NOTIFY message. For an alive notification, all
62
+ # fields will be present. For a byebye notification, location, max_age and
63
+ # server will be nil.
64
+
65
+ class Notification < Advertisement
66
+
67
+ ##
68
+ # Date the notification was received
69
+
70
+ attr_reader :date
71
+
72
+ ##
73
+ # Host the notification was sent from
74
+
75
+ attr_reader :host
76
+
77
+ ##
78
+ # Port the notification was sent from
79
+
80
+ attr_reader :port
81
+
82
+ ##
83
+ # Location of the advertised service or device
84
+
85
+ attr_reader :location
86
+
87
+ ##
88
+ # Maximum age the advertisement is valid for
89
+
90
+ attr_reader :max_age
91
+
92
+ ##
93
+ # Unique Service Name of the advertisement
94
+
95
+ attr_reader :name
96
+
97
+ ##
98
+ # Type of the advertised service or device
99
+
100
+ attr_reader :type
101
+
102
+ ##
103
+ # Server name and version of the advertised service or device
104
+
105
+ attr_reader :server
106
+
107
+ ##
108
+ # \Notification sub-type
109
+
110
+ attr_reader :sub_type
111
+
112
+ ##
113
+ # Parses a NOTIFY advertisement into its component pieces
114
+
115
+ def self.parse(advertisement)
116
+ advertisement = advertisement.gsub "\r", ''
117
+
118
+ advertisement =~ /^host:\s*(\S*)/i
119
+ host, port = $1.split ':'
120
+
121
+ advertisement =~ /^nt:\s*(\S*)/i
122
+ type = $1
123
+
124
+ advertisement =~ /^nts:\s*(\S*)/i
125
+ sub_type = $1
126
+
127
+ advertisement =~ /^usn:\s*(\S*)/i
128
+ name = $1
129
+
130
+ if sub_type == 'ssdp:alive' then
131
+ advertisement =~ /^cache-control:\s*max-age\s*=\s*(\d+)/i
132
+ max_age = Integer $1
133
+
134
+ advertisement =~ /^location:\s*(\S*)/i
135
+ location = URI.parse $1
136
+
137
+ advertisement =~ /^server:\s*(.*)/i
138
+ server = $1.strip
139
+ end
140
+
141
+ new Time.now, max_age, host, port, location, type, sub_type, server, name
142
+ end
143
+
144
+ ##
145
+ # Creates a \new Notification
146
+
147
+ def initialize(date, max_age, host, port, location, type, sub_type,
148
+ server, name)
149
+ @date = date
150
+ @max_age = max_age
151
+ @host = host
152
+ @port = port
153
+ @location = location
154
+ @type = type
155
+ @sub_type = sub_type
156
+ @server = server
157
+ @name = name
158
+ end
159
+
160
+ ##
161
+ # Returns true if this is a notification for a resource being alive
162
+
163
+ def alive?
164
+ sub_type == 'ssdp:alive'
165
+ end
166
+
167
+ ##
168
+ # Returns true if this is a notification for a resource going away
169
+
170
+ def byebye?
171
+ sub_type == 'ssdp:byebye'
172
+ end
173
+
174
+ ##
175
+ # A friendlier inspect
176
+
177
+ def inspect
178
+ location = " #{@location}" if @location
179
+ "#<#{self.class}:0x#{object_id.to_s 16} #{@type} #{@sub_type}#{location}>"
180
+ end
181
+
182
+ end
183
+
184
+ ##
185
+ # Holds information about a M-SEARCH response
186
+
187
+ class Response < Advertisement
188
+
189
+ ##
190
+ # Date response was created or received
191
+
192
+ attr_reader :date
193
+
194
+ ##
195
+ # true if MAN header was understood
196
+
197
+ attr_reader :ext
198
+
199
+ ##
200
+ # URI where this device or service is described
201
+
202
+ attr_reader :location
203
+
204
+ ##
205
+ # Maximum age this advertisement is valid for
206
+
207
+ attr_reader :max_age
208
+
209
+ ##
210
+ # Unique Service Name
211
+
212
+ attr_reader :name
213
+
214
+ ##
215
+ # Server version string
216
+
217
+ attr_reader :server
218
+
219
+ ##
220
+ # Search target
221
+
222
+ attr_reader :target
223
+
224
+ ##
225
+ # Creates a new Response by parsing the text in +response+
226
+
227
+ def self.parse(response)
228
+ response =~ /^cache-control:\s*max-age\s*=\s*(\d+)/i
229
+ max_age = Integer $1
230
+
231
+ response =~ /^date:\s*(.*)/i
232
+ date = $1 ? Time.parse($1) : Time.now
233
+
234
+ ext = !!(response =~ /^ext:/i)
235
+
236
+ response =~ /^location:\s*(\S*)/i
237
+ location = URI.parse $1.strip
238
+
239
+ response =~ /^server:\s*(.*)/i
240
+ server = $1.strip
241
+
242
+ response =~ /^st:\s*(\S*)/i
243
+ target = $1.strip
244
+
245
+ response =~ /^usn:\s*(\S*)/i
246
+ name = $1.strip
247
+
248
+ new date, max_age, location, server, target, name, ext
249
+ end
250
+
251
+ ##
252
+ # Creates a new Response
253
+
254
+ def initialize(date, max_age, location, server, target, name, ext)
255
+ @date = date
256
+ @max_age = max_age
257
+ @location = location
258
+ @server = server
259
+ @target = target
260
+ @name = name
261
+ @ext = ext
262
+ end
263
+
264
+ ##
265
+ # A friendlier inspect
266
+
267
+ def inspect
268
+ "#<#{self.class}:0x#{object_id.to_s 16} #{target} #{location}>"
269
+ end
270
+
271
+ end
272
+
273
+ ##
274
+ # Default broadcast address
275
+
276
+ BROADCAST = '239.255.255.250'
277
+
278
+ ##
279
+ # Default port
280
+
281
+ PORT = 1900
282
+
283
+ ##
284
+ # Default timeout
285
+
286
+ TIMEOUT = 1
287
+
288
+ ##
289
+ # Default packet time to live (hops)
290
+
291
+ TTL = 4
292
+
293
+ ##
294
+ # Broadcast address to use when sending searches and listening for
295
+ # notifications
296
+
297
+ attr_accessor :broadcast
298
+
299
+ ##
300
+ # Listener accessor for tests.
301
+
302
+ attr_accessor :listener # :nodoc:
303
+
304
+ ##
305
+ # Port to use for SSDP searching and listening
306
+
307
+ attr_accessor :port
308
+
309
+ ##
310
+ # Queue accessor for tests
311
+
312
+ attr_accessor :queue # :nodoc:
313
+
314
+ ##
315
+ # Socket accessor for tests
316
+
317
+ attr_accessor :socket # :nodoc:
318
+
319
+ ##
320
+ # Time to wait for SSDP responses
321
+
322
+ attr_accessor :timeout
323
+
324
+ ##
325
+ # TTL for SSDP packets
326
+
327
+ attr_accessor :ttl
328
+
329
+ ##
330
+ # Creates a new SSDP object. Use the accessors to override broadcast, port,
331
+ # timeout or ttl.
332
+
333
+ def initialize
334
+ @broadcast = BROADCAST
335
+ @port = PORT
336
+ @timeout = TIMEOUT
337
+ @ttl = TTL
338
+
339
+ @listener = nil
340
+ @queue = Queue.new
341
+ end
342
+
343
+ ##
344
+ # Discovers UPnP devices sending NOTIFY broadcasts.
345
+ #
346
+ # If given a block, yields each Notification as it is received and never
347
+ # returns. Otherwise, discover waits for timeout seconds and returns all
348
+ # notifications received in that time.
349
+
350
+ def discover
351
+ membership = IPAddr.new(@broadcast).hton + IPAddr.new('0.0.0.0').hton
352
+
353
+ @socket ||= UDPSocket.new
354
+
355
+ @socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
356
+ @socket.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership
357
+
358
+ @socket.bind Socket::INADDR_ANY, @port
359
+
360
+ listen
361
+
362
+ if block_given? then
363
+ loop do
364
+ notification = @queue.pop
365
+
366
+ yield notification
367
+ end
368
+ else
369
+ sleep @timeout
370
+
371
+ notifications = []
372
+ notifications << @queue.pop until @queue.empty?
373
+ notifications
374
+ end
375
+ ensure
376
+ stop_listening
377
+ @socket.close if @socket and not @socket.closed?
378
+ @socket = nil
379
+ end
380
+
381
+ ##
382
+ # Listens for UDP packets from devices in a Thread and enqueues them for
383
+ # processing. Requires a socket from search or discover.
384
+
385
+ def listen
386
+ return @listener if @listener and @listener.alive?
387
+
388
+ @listener = Thread.start do
389
+ loop do
390
+ response = @socket.recvfrom(1024).first
391
+
392
+ begin
393
+ @queue << parse(response)
394
+ rescue
395
+ puts $!.message
396
+ puts $!.backtrace
397
+ end
398
+ end
399
+ end
400
+ end
401
+
402
+ ##
403
+ # Returns a Notification or Response created from +response+.
404
+
405
+ def parse(response)
406
+ case response
407
+ when /\ANOTIFY/ then
408
+ Notification.parse response
409
+ when /\AHTTP/ then
410
+ Response.parse response
411
+ else
412
+ raise Error, "Unknown response #{response[/\A.*$/]}"
413
+ end
414
+ end
415
+
416
+ ##
417
+ # Broadcasts M-SEARCH requests looking for +targets+. Waits timeout seconds
418
+ # for responses then returns the collected responses.
419
+ #
420
+ # Supply no arguments to search for all devices and services.
421
+ #
422
+ # Supply <tt>:root</tt> to search for root devices only.
423
+ #
424
+ # Supply <tt>[:device, 'device_type:version']</tt> to search for a specific
425
+ # device type.
426
+ #
427
+ # Supply <tt>[:service, 'service_type:version']</tt> to search for a
428
+ # specific service type.
429
+ #
430
+ # Supply <tt>"uuid:..."</tt> to search for a UUID.
431
+ #
432
+ # Supply <tt>"urn:..."</tt> to search for a URN.
433
+
434
+ def search(*targets)
435
+ @socket ||= UDPSocket.new
436
+
437
+ @socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
438
+
439
+ if targets.empty? then
440
+ send_search 'ssdp:all'
441
+ else
442
+ targets.each do |target|
443
+ if target == :root then
444
+ send_search 'upnp:rootdevice'
445
+ elsif Array === target and target.first == :device then
446
+ target = [UPnP::DEVICE_SCHEMA_PREFIX, target.last]
447
+ send_search target.join(':')
448
+ elsif Array === target and target.first == :service then
449
+ target = [UPnP::SERVICE_SCHEMA_PREFIX, target.last]
450
+ send_search target.join(':')
451
+ elsif String === target and target =~ /\A(urn|uuid|ssdp):/ then
452
+ send_search target
453
+ end
454
+ end
455
+ end
456
+
457
+ listen
458
+ sleep @timeout
459
+
460
+ responses = []
461
+ responses << @queue.pop until @queue.empty?
462
+ responses
463
+ ensure
464
+ stop_listening
465
+ @socket.close if @socket and not @socket.closed?
466
+ @socket = nil
467
+ end
468
+
469
+ ##
470
+ # Builds and sends an M-SEARCH request looking for +search_target+.
471
+
472
+ def send_search(search_target)
473
+ http_request = <<HTTP_REQUEST
474
+ M-SEARCH * HTTP/1.1\r
475
+ HOST: #{@broadcast}:#{@port}\r
476
+ MAN: "ssdp:discover"\r
477
+ MX: #{@timeout}\r
478
+ ST: #{search_target}\r
479
+ \r
480
+ HTTP_REQUEST
481
+
482
+ @socket.send http_request, 0, @broadcast, @port
483
+ end
484
+
485
+ ##
486
+ # Stops and clears the listen thread.
487
+
488
+ def stop_listening
489
+ @listener.kill if @listener
490
+ @queue = Queue.new
491
+ @listener = nil
492
+ end
493
+
494
+ end
495
+