cosmos 4.0.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -5
  3. data/Manifest.txt +11 -1
  4. data/README.md +3 -2
  5. data/Rakefile +18 -4
  6. data/appveyor.yml +19 -0
  7. data/cosmos.gemspec +14 -3
  8. data/data/config/cmd_tlm_server.yaml +3 -0
  9. data/data/crc.txt +63 -60
  10. data/demo/config/targets/INST/cmd_tlm_server.txt +1 -0
  11. data/demo/config/targets/INST/cmd_tlm_server2.txt +7 -0
  12. data/demo/config/tools/cmd_sequence/cmd_sequence.txt +2 -0
  13. data/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt +8 -12
  14. data/demo/config/tools/cmd_tlm_server/cmd_tlm_server2.txt +7 -9
  15. data/demo/lib/cmd_sequence_exporter.rb +52 -0
  16. data/demo/lib/example_background_task.rb +1 -0
  17. data/demo/procedures/replay_test.rb +32 -0
  18. data/ext/cosmos/ext/structure/structure.c +39 -3
  19. data/install/config/tools/cmd_tlm_server/cmd_tlm_server.txt +1 -0
  20. data/install/config/tools/launcher/launcher.txt +2 -0
  21. data/lib/cosmos/config/config_parser.rb +2 -0
  22. data/lib/cosmos/core_ext/io.rb +89 -60
  23. data/lib/cosmos/gui/qt.rb +5 -8
  24. data/lib/cosmos/gui/qt_tool.rb +8 -8
  25. data/lib/cosmos/gui/text/ruby_editor.rb +12 -12
  26. data/lib/cosmos/gui/utilities/script_module_gui.rb +9 -9
  27. data/lib/cosmos/gui/widgets/realtime_button_bar.rb +18 -17
  28. data/lib/cosmos/interfaces/protocols/fixed_protocol.rb +2 -2
  29. data/lib/cosmos/interfaces/protocols/template_protocol.rb +3 -0
  30. data/lib/cosmos/interfaces/udp_interface.rb +27 -14
  31. data/lib/cosmos/io/buffered_file.rb +0 -1
  32. data/lib/cosmos/io/json_drb.rb +134 -214
  33. data/lib/cosmos/io/json_drb_object.rb +22 -61
  34. data/lib/cosmos/io/json_drb_rack.rb +79 -0
  35. data/lib/cosmos/io/json_rpc.rb +27 -0
  36. data/lib/cosmos/io/udp_sockets.rb +102 -58
  37. data/lib/cosmos/packets/commands.rb +1 -1
  38. data/lib/cosmos/packets/structure.rb +1 -1
  39. data/lib/cosmos/packets/structure_item.rb +37 -5
  40. data/lib/cosmos/script/cmd_tlm_server.rb +76 -2
  41. data/lib/cosmos/script/replay.rb +60 -0
  42. data/lib/cosmos/script/script.rb +20 -2
  43. data/lib/cosmos/script/scripting.rb +9 -9
  44. data/lib/cosmos/script/tools.rb +14 -0
  45. data/lib/cosmos/system/system.rb +185 -92
  46. data/lib/cosmos/system/target.rb +1 -1
  47. data/lib/cosmos/tools/cmd_sequence/cmd_sequence.rb +44 -4
  48. data/lib/cosmos/tools/cmd_sequence/sequence_item.rb +4 -0
  49. data/lib/cosmos/tools/cmd_sequence/sequence_list.rb +7 -0
  50. data/lib/cosmos/tools/cmd_tlm_server/api.rb +347 -20
  51. data/lib/cosmos/tools/cmd_tlm_server/background_tasks.rb +3 -0
  52. data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb +329 -111
  53. data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_config.rb +13 -0
  54. data/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb +261 -95
  55. data/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb +46 -35
  56. data/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb +18 -8
  57. data/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb +39 -28
  58. data/lib/cosmos/tools/cmd_tlm_server/gui/replay_tab.rb +242 -0
  59. data/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb +24 -8
  60. data/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb +18 -6
  61. data/lib/cosmos/tools/cmd_tlm_server/limits_groups_background_task.rb +5 -4
  62. data/lib/cosmos/tools/cmd_tlm_server/replay_backend.rb +375 -0
  63. data/lib/cosmos/tools/cmd_tlm_server/routers.rb +10 -2
  64. data/lib/cosmos/tools/data_viewer/data_viewer.rb +40 -5
  65. data/lib/cosmos/tools/handbook_creator/handbook_creator_config.rb +18 -20
  66. data/lib/cosmos/tools/launcher/launcher_config.rb +5 -16
  67. data/lib/cosmos/tools/limits_monitor/limits_monitor.rb +65 -39
  68. data/lib/cosmos/tools/packet_viewer/packet_viewer.rb +19 -0
  69. data/lib/cosmos/tools/replay/replay.rb +5 -505
  70. data/lib/cosmos/tools/script_runner/script_audit.rb +1 -0
  71. data/lib/cosmos/tools/script_runner/script_runner.rb +3 -4
  72. data/lib/cosmos/tools/script_runner/script_runner_config.rb +3 -4
  73. data/lib/cosmos/tools/script_runner/script_runner_frame.rb +44 -23
  74. data/lib/cosmos/tools/test_runner/results_writer.rb +4 -0
  75. data/lib/cosmos/tools/test_runner/test_runner.rb +0 -3
  76. data/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb +6 -2
  77. data/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb +26 -1
  78. data/lib/cosmos/tools/tlm_viewer/screen.rb +24 -1
  79. data/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb +25 -0
  80. data/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb +24 -14
  81. data/lib/cosmos/top_level.rb +34 -24
  82. data/lib/cosmos/utilities/csv.rb +60 -8
  83. data/lib/cosmos/version.rb +5 -5
  84. data/spec/config/config_parser_spec.rb +10 -1
  85. data/spec/core_ext/socket_spec.rb +4 -2
  86. data/spec/gui/utilities/script_module_gui_spec.rb +102 -0
  87. data/spec/install/config/data/data.txt +1 -0
  88. data/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt +2 -0
  89. data/spec/interfaces/cmd_tlm_server_interface_spec.rb +1 -2
  90. data/spec/interfaces/protocols/template_protocol_spec.rb +72 -2
  91. data/spec/interfaces/serial_interface_spec.rb +1 -1
  92. data/spec/interfaces/udp_interface_spec.rb +14 -0
  93. data/spec/io/buffered_file_spec.rb +37 -0
  94. data/spec/io/json_drb_object_spec.rb +2 -15
  95. data/spec/io/json_drb_spec.rb +61 -121
  96. data/spec/io/udp_sockets_spec.rb +42 -2
  97. data/spec/packet_logs/packet_log_reader_spec.rb +5 -2
  98. data/spec/packets/binary_accessor_spec.rb +1 -1
  99. data/spec/packets/packet_item_spec.rb +1 -1
  100. data/spec/packets/structure_item_spec.rb +5 -6
  101. data/spec/script/cmd_tlm_server_spec.rb +39 -4
  102. data/spec/script/commands_disconnect_spec.rb +1 -1
  103. data/spec/script/commands_spec.rb +2 -1
  104. data/spec/script/scripting_spec.rb +18 -3
  105. data/spec/script/telemetry_spec.rb +5 -0
  106. data/spec/spec_helper.rb +43 -26
  107. data/spec/streams/tcpip_socket_stream_spec.rb +2 -2
  108. data/spec/system/system_spec.rb +11 -9
  109. data/spec/system/target_spec.rb +3 -0
  110. data/spec/tools/cmd_tlm_server/api_spec.rb +543 -29
  111. data/spec/tools/cmd_tlm_server/background_task_spec.rb +2 -2
  112. data/spec/tools/cmd_tlm_server/background_tasks_spec.rb +31 -75
  113. data/spec/tools/cmd_tlm_server/cmd_tlm_server_config_spec.rb +199 -66
  114. data/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb +85 -9
  115. data/spec/tools/cmd_tlm_server/interface_thread_spec.rb +29 -127
  116. data/spec/tools/cmd_tlm_server/router_thread_spec.rb +10 -50
  117. data/spec/tools/launcher/launcher_config_spec.rb +1 -1
  118. data/spec/tools/table_manager/table_item_spec.rb +1 -1
  119. data/spec/tools/table_manager/tablemanager_core_spec.rb +4 -4
  120. data/spec/top_level/top_level_spec.rb +151 -3
  121. data/spec/utilities/csv_spec.rb +24 -5
  122. metadata +61 -9
  123. data/lib/cosmos/tools/replay/replay_server.rb +0 -91
@@ -15,6 +15,8 @@ require 'json'
15
15
  require 'drb/acl'
16
16
  require 'drb/drb'
17
17
  require 'cosmos/io/json_drb'
18
+ require 'uri'
19
+ require 'httpclient'
18
20
 
19
21
  module Cosmos
20
22
 
@@ -36,7 +38,7 @@ module Cosmos
36
38
  def initialize(hostname, port, connect_timeout = 1.0)
37
39
  hostname = '127.0.0.1' if (hostname.to_s.upcase == 'LOCALHOST')
38
40
  begin
39
- @addr = Socket.pack_sockaddr_in(port, hostname)
41
+ addr = Socket.pack_sockaddr_in(port, hostname)
40
42
  rescue => error
41
43
  if error.message =~ /getaddrinfo/
42
44
  raise "Invalid hostname: #{hostname}"
@@ -46,9 +48,9 @@ module Cosmos
46
48
  end
47
49
  @hostname = hostname
48
50
  @port = port
51
+ @uri = URI("http://#{@hostname}:#{@port}")
52
+ @http = nil
49
53
  @mutex = Mutex.new
50
- @socket = nil
51
- @pipe_reader, @pipe_writer = IO.pipe
52
54
  @id = 0
53
55
  @request_in_progress = false
54
56
  @connect_timeout = connect_timeout
@@ -56,17 +58,12 @@ module Cosmos
56
58
  @shutdown = false
57
59
  end
58
60
 
59
- # Disconnects from the JSON server
61
+ # Disconnects from http server
60
62
  def disconnect
61
- Cosmos.close_socket(@socket)
62
- @pipe_writer.write('.')
63
- # Cannot set @socket to nil here because this method can be called by
64
- # other threads and @socket being nil would cause unexpected errors in method_missing
65
- # Also don't want to take the mutex so that we can interrupt method_missing if necessary
66
- # Only method_missing can set @socket to nil
63
+ @http.reset_all if @http
67
64
  end
68
65
 
69
- # Permanently disconnects from the JSON server
66
+ # Permanently disconnects from the http server
70
67
  def shutdown
71
68
  @shutdown = true
72
69
  disconnect()
@@ -81,19 +78,14 @@ module Cosmos
81
78
  # protocol a DRb::DRbConnError exception is raised.
82
79
  def method_missing(method_name, *method_params)
83
80
  @mutex.synchronize do
84
- # This flag and loop are used to automatically reconnect and retry if something goes
85
- # wrong on the first attempt writing to the socket. Sockets can become disconnected
86
- # between function calls, but as long as the remote server is back up and running the
87
- # call should succeed even when it discovers a broken socket on the first attempt.
88
81
  first_try = true
89
82
  loop do
90
83
  raise DRb::DRbConnError, "Shutdown" if @shutdown
91
- connect() if !@socket or @socket.closed? or @request_in_progress
84
+ connect() if !@http or @request_in_progress
92
85
 
93
86
  response = make_request(method_name, method_params, first_try)
94
87
  unless response
95
88
  disconnect()
96
- @socket = nil
97
89
  was_first_try = first_try
98
90
  first_try = false
99
91
  next if was_first_try
@@ -108,44 +100,13 @@ module Cosmos
108
100
  def connect
109
101
  if @request_in_progress
110
102
  disconnect()
111
- @socket = nil
112
103
  @request_in_progress = false
113
104
  end
114
105
  begin
115
- addr = Socket.pack_sockaddr_in(@port, @hostname)
116
- @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
117
- @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
118
- @pipe_reader, @pipe_writer = IO.pipe
119
- begin
120
- @socket.connect_nonblock(addr)
121
- rescue IO::WaitWritable
122
- begin
123
- _, sockets, _ = IO.select(nil, [@socket], nil, @connect_timeout) # wait 3-way handshake completion
124
- rescue IOError, Errno::ENOTSOCK
125
- disconnect()
126
- @socket = nil
127
- raise "Connect canceled"
128
- end
129
- if sockets and !sockets.empty?
130
- begin
131
- @socket.connect_nonblock(addr) # check connection failure
132
- rescue IOError, Errno::ENOTSOCK
133
- disconnect()
134
- @socket = nil
135
- raise "Connect canceled"
136
- rescue Errno::EINPROGRESS
137
- retry
138
- rescue Errno::EISCONN, Errno::EALREADY
139
- end
140
- else
141
- disconnect()
142
- @socket = nil
143
- raise "Connect timeout"
144
- end
145
- rescue IOError, Errno::ENOTSOCK
146
- disconnect()
147
- @socket = nil
148
- raise "Connect canceled"
106
+ if !@http
107
+ @http = HTTPClient.new
108
+ @http.connect_timeout = @connect_timeout
109
+ @http.receive_timeout = nil # Allow long polling
149
110
  end
150
111
  rescue => e
151
112
  raise DRb::DRbConnError, e.message
@@ -158,17 +119,19 @@ module Cosmos
158
119
 
159
120
  request_data = request.to_json(:allow_nan => true)
160
121
  begin
161
- STDOUT.puts "Request:\n" if JsonDRb.debug?
122
+ STDOUT.puts "\nRequest:\n" if JsonDRb.debug?
162
123
  STDOUT.puts request_data if JsonDRb.debug?
163
124
  @request_in_progress = true
164
- JsonDRb.send_data(@socket, request_data)
165
- response_data = JsonDRb.receive_message(@socket, '', @pipe_reader)
125
+ headers = {'Content-Type' => 'application/json-rpc'}
126
+ res = @http.post(@uri,
127
+ :body => request_data,
128
+ :header => headers)
129
+ response_data = res.body
166
130
  @request_in_progress = false
167
- STDOUT.puts "\nResponse:\n" if JsonDRb.debug?
131
+ STDOUT.puts "Response:\n" if JsonDRb.debug?
168
132
  STDOUT.puts response_data if JsonDRb.debug?
169
133
  rescue => e
170
134
  disconnect()
171
- @socket = nil
172
135
  return false if first_try
173
136
  raise DRb::DRbConnError, e.message, e.backtrace
174
137
  end
@@ -177,7 +140,7 @@ module Cosmos
177
140
 
178
141
  def handle_response(response_data)
179
142
  # The code below will always either raise or return breaking out of the loop
180
- if response_data
143
+ if response_data and response_data.to_s.length > 0
181
144
  response = JsonRpcResponse.from_json(response_data)
182
145
  if JsonRpcErrorResponse === response
183
146
  if response.error.data
@@ -189,10 +152,8 @@ module Cosmos
189
152
  return response.result
190
153
  end
191
154
  else
192
- # Socket was closed by server
193
155
  disconnect()
194
- @socket = nil
195
- raise DRb::DRbConnError, "Socket closed by server"
156
+ raise DRb::DRbConnError, "No response from server"
196
157
  end
197
158
  end
198
159
 
@@ -0,0 +1,79 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2017 Ball Aerospace & Technologies Corp.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+
11
+ require 'rack'
12
+
13
+ module Cosmos
14
+
15
+ # JsonDrbRack implements a rack application that can be served by a
16
+ # webserver to process COSMOS json_drb requests via http.
17
+ class JsonDrbRack
18
+ # @param drb [JsonDRb] - An instance of the JsonDRb class that'll be used
19
+ # to process the JSON request and generate a response
20
+ def initialize(drb)
21
+ @drb = drb
22
+ end
23
+
24
+ # Handles a request.
25
+ #
26
+ # @param env [Hash] - A rack env hash, can be turned into a Rack::Request
27
+ # @return [Integer, Hash, [String]] - Http response code, content headers,
28
+ # response body
29
+ def call(env)
30
+ request = Rack::Request.new(env)
31
+
32
+ # ACL allow_addr? function takes address in the form returned by
33
+ # IPSocket.peeraddr.
34
+ req_addr = ["AF_INET", request.port, request.host.to_s, request.ip.to_s]
35
+
36
+ if @drb.acl and !@drb.acl.allow_addr?(req_addr)
37
+ status = 403
38
+ content_type = "text/plain"
39
+ body = "Forbidden"
40
+ elsif request.post?
41
+ status, content_type, body = handle_post(request)
42
+ else
43
+ status = 405
44
+ content_type = "text/plain"
45
+ body = "Request not allowed"
46
+ end
47
+
48
+ return status, {'Content-Type' => content_type}, [body]
49
+ end
50
+
51
+ # Handles an http post.
52
+ #
53
+ # @param request [Rack::Request] - A rack post request
54
+ # @return [Integer, String, String] - Http response code, content type,
55
+ # response body.
56
+ def handle_post(request)
57
+ request_data = request.body.read
58
+ start_time = Time.now.sys
59
+ response_data, error_code = @drb.process_request(request_data, start_time)
60
+
61
+ # Convert json error code into html status code
62
+ # see http://www.jsonrpc.org/historical/json-rpc-over-http.html#errors
63
+ if error_code
64
+ case error_code
65
+ when JsonRpcError::ErrorCode::PARSE_ERROR then status = 500 # Internal server error
66
+ when JsonRpcError::ErrorCode::INVALID_REQUEST then status = 400 # Bad request
67
+ when JsonRpcError::ErrorCode::METHOD_NOT_FOUND then status = 404 # Not found
68
+ when JsonRpcError::ErrorCode::INVALID_PARAMS then status = 500 # Internal server error
69
+ when JsonRpcError::ErrorCode::INTERNAL_ERROR then status = 500 # Internal server error
70
+ else status = 500 # Internal server error
71
+ end
72
+ else
73
+ status = 200 # OK
74
+ end
75
+
76
+ return status, "application/json-rpc", response_data
77
+ end
78
+ end
79
+ end
@@ -60,6 +60,23 @@ class Numeric
60
60
  def as_json(options = nil) self end #:nodoc:
61
61
  end
62
62
 
63
+ class Float
64
+ def self.json_create(object)
65
+ case object['raw']
66
+ when "Infinity" then Float::INFINITY
67
+ when "-Infinity" then -Float::INFINITY
68
+ when "NaN" then Float::NAN
69
+ end
70
+ end
71
+
72
+ def as_json(options = nil)
73
+ return {"json_class"=>Float, "raw"=>"Infinity"} if self.infinite? == 1
74
+ return {"json_class"=>Float, "raw"=>"-Infinity"} if self.infinite? == -1
75
+ return {"json_class"=>Float, "raw"=>"NaN"} if self.nan?
76
+ return self
77
+ end
78
+ end
79
+
63
80
  class Regexp
64
81
  def as_json(options = nil) to_s end #:nodoc:
65
82
  end
@@ -324,6 +341,16 @@ module Cosmos
324
341
  # Represents a JSON Remote Procedure Call Error
325
342
  class JsonRpcError < JsonRpc
326
343
 
344
+ # Enumeration of JSON RPC error codes
345
+ class ErrorCode
346
+ PARSE_ERROR = -32700
347
+ INVALID_REQUEST = -32600
348
+ METHOD_NOT_FOUND = -32601
349
+ INVALID_PARAMS = -32602
350
+ INTERNAL_ERROR = -32603
351
+ OTHER_ERROR = -1
352
+ end
353
+
327
354
  # @param code [Integer] The error type that occurred
328
355
  # @param message [String] A short description of the error
329
356
  # @param data [Hash] Additional information about the error
@@ -18,38 +18,55 @@ Socket::IP_MULTICAST_TTL = 10 unless Socket.const_defined?('IP_MULTICAST_TTL')
18
18
 
19
19
  module Cosmos
20
20
 
21
- # Creates a UDPSocket and implements a non-blocking write.
22
- class UdpWriteSocket
21
+ class UdpReadWriteSocket
22
+ # @param bind_port [Integer[ Port to write data out from and receive data on (0 = randomly assigned)
23
+ # @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses)
24
+ # @param external_port [Integer] External port to write to
25
+ # @param external_address [String] External host to send data to
26
+ # @param multicast_interface_address [String] Local outgoing interface to send multicast packets from
27
+ # @param ttl [Integer] Time To Live for outgoing multicast packets
28
+ # @param read_multicast [Boolean] Whether or not to try to read from the external address as multicast
29
+ # @param write_multicast [Boolean] Whether or not to write to the external address as multicast
30
+ def initialize(
31
+ bind_port = 0,
32
+ bind_address = "0.0.0.0",
33
+ external_port = nil,
34
+ external_address = nil,
35
+ multicast_interface_address = nil,
36
+ ttl = 1,
37
+ read_multicast = true,
38
+ write_multicast = true)
23
39
 
24
- # @param dest_address [String] Host to send data to
25
- # @param dest_port [Integer] Port to send data to
26
- # @param src_port [Integer[ Port to send data out from
27
- # @param interface_address [String] Local outgoing interface to send from
28
- # @param ttl [Integer] Time To Live for outgoing packets
29
- def initialize(dest_address,
30
- dest_port,
31
- src_port = nil,
32
- interface_address = nil,
33
- ttl = 1,
34
- bind_address = "0.0.0.0")
35
40
  @socket = UDPSocket.new
36
41
 
37
42
  # Basic setup to reuse address
38
43
  @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
39
44
 
40
- # Set source port if given
41
- @socket.bind(bind_address, src_port) if src_port
45
+ # Bind to local address and port - This sets recv port, write_src port, recv_address, and write_src_address
46
+ @socket.bind(bind_address, bind_port) if (bind_address and bind_port)
42
47
 
43
48
  # Default send to the specified address and port
44
- @socket.connect(dest_address, dest_port)
49
+ @socket.connect(external_address, external_port) if (external_address and external_port)
45
50
 
46
51
  # Handle multicast
47
- if UdpWriteSocket.multicast?(dest_address)
48
- # Basic setup set time to live
49
- @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl.to_i)
52
+ if UdpReadWriteSocket.multicast?(external_address)
53
+ if write_multicast
54
+ # Basic setup set time to live
55
+ @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl.to_i)
56
+
57
+ # Set outgoing interface
58
+ @socket.setsockopt(
59
+ Socket::IPPROTO_IP,
60
+ Socket::IP_MULTICAST_IF,
61
+ IPAddr.new(multicast_interface_address).hton) if multicast_interface_address
62
+ end
50
63
 
51
- # Set outgoing interface
52
- @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, IPAddr.new(interface_address).hton) if interface_address
64
+ # Receive messages sent to the multicast address
65
+ if read_multicast
66
+ multicast_interface_address = "0.0.0.0" unless multicast_interface_address
67
+ membership = IPAddr.new(external_address).hton + IPAddr.new(multicast_interface_address).hton
68
+ @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
69
+ end
53
70
  end
54
71
  end
55
72
 
@@ -78,6 +95,23 @@ module Cosmos
78
95
  end
79
96
  end
80
97
 
98
+ # @param read_timeout [Float] Time in seconds to wait for the read to
99
+ # complete
100
+ def read(read_timeout = nil)
101
+ data = nil
102
+ begin
103
+ data, _ = @socket.recvfrom_nonblock(65536)
104
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
105
+ result = IO.fast_select([@socket], nil, nil, read_timeout)
106
+ if result
107
+ retry
108
+ else
109
+ raise Timeout::Error, "Read Timeout"
110
+ end
111
+ end
112
+ data
113
+ end
114
+
81
115
  # Defer all methods to the UDPSocket
82
116
  def method_missing(method, *args, &block)
83
117
  @socket.__send__(method, *args, &block)
@@ -105,47 +139,57 @@ module Cosmos
105
139
  end
106
140
  end
107
141
 
108
- # Creates a UDPSocket and implements a non-blocking read.
109
- class UdpReadSocket
110
-
111
- # @param recv_port [Integer] Port to receive data on
112
- # @param multicast_address [String] Address to add multicast
113
- def initialize(recv_port = 0, multicast_address = nil, interface_address = nil, bind_address = "0.0.0.0")
114
- @socket = UDPSocket.new
115
-
116
- # Basic setup to reuse address
117
- @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
118
-
119
- # bind to port
120
- @socket.bind(bind_address, recv_port)
142
+ # Creates a UDPSocket and implements a non-blocking write.
143
+ class UdpWriteSocket < UdpReadWriteSocket
121
144
 
122
- if UdpWriteSocket.multicast?(multicast_address)
123
- interface_address = "0.0.0.0" unless interface_address
124
- membership = IPAddr.new(multicast_address).hton + IPAddr.new(interface_address).hton
125
- @socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
126
- end
145
+ # @param dest_address [String] Host to send data to
146
+ # @param dest_port [Integer] Port to send data to
147
+ # @param src_port [Integer[ Port to send data out from
148
+ # @param multicast_interface_address [String] Local outgoing interface to send multicast packets from
149
+ # @param ttl [Integer] Time To Live for outgoing packets
150
+ # @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses)
151
+ def initialize(
152
+ dest_address,
153
+ dest_port,
154
+ src_port = nil,
155
+ multicast_interface_address = nil,
156
+ ttl = 1,
157
+ bind_address = "0.0.0.0")
158
+
159
+ super(
160
+ src_port,
161
+ bind_address,
162
+ dest_port,
163
+ dest_address,
164
+ multicast_interface_address,
165
+ ttl,
166
+ false,
167
+ true)
127
168
  end
169
+ end
128
170
 
129
- # @param read_timeout [Float] Time in seconds to wait for the read to
130
- # complete
131
- def read(read_timeout = nil)
132
- data = nil
133
- begin
134
- data, _ = @socket.recvfrom_nonblock(65536)
135
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
136
- result = IO.fast_select([@socket], nil, nil, read_timeout)
137
- if result
138
- retry
139
- else
140
- raise Timeout::Error, "Read Timeout"
141
- end
142
- end
143
- data
144
- end
171
+ # Creates a UDPSocket and implements a non-blocking read.
172
+ class UdpReadSocket < UdpReadWriteSocket
145
173
 
146
- # Defer all methods to the UDPSocket
147
- def method_missing(method, *args, &block)
148
- @socket.__send__(method, *args, &block)
174
+ # @param recv_port [Integer] Port to receive data on
175
+ # @param multicast_address [String] Address to add multicast
176
+ # @param multicast_interface_address [String] Local incoming interface to receive multicast packets on
177
+ # @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses)
178
+ def initialize(
179
+ recv_port = 0,
180
+ multicast_address = nil,
181
+ multicast_interface_address = nil,
182
+ bind_address = "0.0.0.0")
183
+
184
+ super(
185
+ recv_port,
186
+ bind_address,
187
+ nil,
188
+ multicast_address,
189
+ multicast_interface_address,
190
+ 1,
191
+ true,
192
+ false)
149
193
  end
150
194
  end
151
195