rsmp 0.2 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5561e779b1d3e8aa85dd53d2c4e4ce5a630f9405f21b095f413de002021a2201
4
- data.tar.gz: 2546949c1d79c588a8192feb00eee640cd7b1f8f9b6327b7c78554dbd63935e0
3
+ metadata.gz: 9a3b1b8a67bdc7a3c8887ea606d3539d934121d99597f1a77f697d8259daba97
4
+ data.tar.gz: 03f420b48a4eff2f3b6978eed5053a838a87da4f507eb2ddb2f0c15cb0e3e7b4
5
5
  SHA512:
6
- metadata.gz: b74bae63684ac30eca6f92446ba3a3fe670d25bc6daae7fd9a980363c13ddf66bc573f16057d58c8a3d80995e67b8c364502fcc05fe3891036afe54ec517a822
7
- data.tar.gz: a40e8a4581a3618e0d51cd33a179fec599042fdabf811b1809b628b472b96e18b49416ad6f05692b8eeb79ba3bec26330a0775b42bdfdc56c9a4494264667b30
6
+ metadata.gz: 9fef0b5ea4b2e5481d3be7992e76c7cddb1d20db49df06c86d9b90faa37b56d458fde9cba36e14ea085e96e67f61dbe67f1a82a49401d360cb48900c6138b27d
7
+ data.tar.gz: a14a735fccf39e8584e3d9ab43a1fdb727c279f395c138d9c7c50f7a8d491265c0aeb3aa67ee071642195bd7a15e0339de4785b5230bbed624965ef3619a231e
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.2)
4
+ rsmp (0.3.0)
5
5
  async (~> 1.29.1)
6
6
  async-io (~> 1.32.1)
7
7
  colorize (~> 0.8.1)
@@ -24,12 +24,12 @@ GEM
24
24
  cucumber (>= 2.4, < 7.0)
25
25
  rspec-expectations (~> 3.4)
26
26
  thor (~> 1.0)
27
- async (1.29.1)
27
+ async (1.29.2)
28
28
  console (~> 1.10)
29
29
  nio4r (~> 2.3)
30
30
  timers (~> 4.1)
31
- async-io (1.32.1)
32
- async (~> 1.14)
31
+ async-io (1.32.2)
32
+ async
33
33
  builder (3.2.4)
34
34
  childprocess (4.1.0)
35
35
  colorize (0.8.1)
@@ -88,7 +88,7 @@ GEM
88
88
  mime-types-data (3.2021.0225)
89
89
  minitest (5.14.4)
90
90
  multi_test (0.1.2)
91
- nio4r (2.5.7)
91
+ nio4r (2.5.8)
92
92
  protobuf-cucumber (3.10.8)
93
93
  activesupport (>= 3.2)
94
94
  middleware
@@ -1,21 +1,19 @@
1
- # Collects matching ingoing and/or outgoing messages and
2
- # wakes up the client once the desired amount has been collected.
3
- # Can listen for ingoing and/or outgoing messages.
4
-
5
1
  module RSMP
6
- class Collector < Listener
7
2
 
3
+ # Collects ingoing and/or outgoing messages.
4
+ # Can filter by message type and wakes up the client once the desired number of messages has been collected.
5
+ class Collector < Listener
8
6
  attr_reader :condition, :messages, :done
9
7
 
10
8
  def initialize proxy, options={}
11
9
  super proxy, options
10
+ @options = options.clone
12
11
  @ingoing = options[:ingoing] == nil ? true : options[:ingoing]
13
12
  @outgoing = options[:outgoing] == nil ? false : options[:outgoing]
14
- @messages = []
15
13
  @condition = Async::Notification.new
16
- @done = false
17
- @options = options
18
- @num = options[:num]
14
+ @title = options[:title] || [@options[:type]].flatten.join('/')
15
+ @options[:timeout] ||= 1
16
+ reset
19
17
  end
20
18
 
21
19
  def inspect
@@ -34,17 +32,9 @@ module RSMP
34
32
  @condition.wait
35
33
  end
36
34
 
37
- def collect_for task, duration
38
- siphon do
39
- task.sleep duration
40
- end
41
- end
42
-
43
35
  def collect task, options={}, &block
44
- @num = options[:num] if options[:num]
45
- @options[:timeout] = options[:timeout] if options[:timeout]
36
+ @options.merge! options
46
37
  @block = block
47
-
48
38
  unless @done
49
39
  listen do
50
40
  task.with_timeout(@options[:timeout]) do
@@ -52,51 +42,97 @@ module RSMP
52
42
  end
53
43
  end
54
44
  end
45
+ return @error if @error
46
+ self
47
+ rescue Async::TimeoutError
48
+ str = "Did not receive #{@title}"
49
+ str << " in response to #{options[:m_id]}" if options[:m_id]
50
+ str << " within #{@options[:timeout]}s"
51
+ raise RSMP::TimeoutError.new str
52
+ end
55
53
 
56
- if @num == 1
57
- @messages = @messages.first # if one message was requested, return it instead of array
58
- else
59
- @messages = @messages.first @num # return array, but ensure we never return more than requested
60
- end
61
- @messages
54
+ def result
55
+ return @messages.first if @options[:num] == 1 # if one message was requested, return it instead of array
56
+ @messages.first @options[:num] # return array, but ensure we never return more than requested
62
57
  end
63
58
 
64
59
  def reset
65
- @message.clear
60
+ @messages = []
61
+ @error = nil
66
62
  @done = false
67
63
  end
68
64
 
65
+ # Check for MessageNotAck
66
+ # If the original request identified by @m_id is rejected, set the result to an exception,
67
+ # which will be returned by the async task and stored as the task result.
68
+ # When the parent task call wait() on the task, the exception will be raised in the parent task.
69
+ def check_not_ack message
70
+ return unless @options[:m_id]
71
+ if message.is_a?(MessageNotAck)
72
+ if message.attribute('oMId') == @options[:m_id]
73
+ m_id_short = RSMP::Message.shorten_m_id @options[:m_id], 8
74
+ @error = RSMP::MessageRejected.new("#{@title} #{m_id_short} was rejected: #{message.attribute('rea')}")
75
+ complete
76
+ end
77
+ false
78
+ end
79
+ end
80
+
81
+ # Handle message. and return true when we're done collecting
69
82
  def notify message
70
83
  raise ArgumentError unless message
84
+ check_not_ack(message)
71
85
  return true if @done
72
- return if message.direction == :in && @ingoing == false
73
- return if message.direction == :out && @outgoing == false
74
- if matches? message
75
- @messages << message
76
- if @num && @messages.size >= @num
77
- @done = true
78
- @proxy.remove_listener self
79
- @condition.signal
80
- end
86
+ check_match message
87
+ complete if done?
88
+ @done
89
+ end
90
+
91
+ # Match message against our collection criteria
92
+ def check_match message
93
+ matched = match? message
94
+ if matched == true
95
+ keep message
96
+ elsif matched == false
97
+ forget message
81
98
  end
82
99
  end
83
100
 
84
- def matches? message
85
- raise ArgumentError unless message
101
+ def done?
102
+ @options[:num] && @messages.size >= @options[:num]
103
+ end
104
+
105
+ def complete
106
+ @done = true
107
+ @proxy.remove_listener self
108
+ @condition.signal
109
+ end
110
+
111
+ def keep message
112
+ @messages << message
113
+ end
114
+
115
+ def forget message
116
+ @messages.delete message
117
+ end
86
118
 
119
+ def match? message
120
+ raise ArgumentError unless message
121
+ return if message.direction == :in && @ingoing == false
122
+ return if message.direction == :out && @outgoing == false
87
123
  if @options[:type]
88
- return false if message == nil
124
+ return if message == nil
89
125
  if @options[:type].is_a? Array
90
- return false unless @options[:type].include? message.type
126
+ return unless @options[:type].include? message.type
91
127
  else
92
- return false unless message.type == @options[:type]
128
+ return unless message.type == @options[:type]
93
129
  end
94
130
  end
95
131
  if @options[:component]
96
- return false if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
132
+ return if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
97
133
  end
98
134
  if @block
99
- return false if @block.call(message) == false
135
+ return if @block.call(message) == false
100
136
  end
101
137
  true
102
138
  end
data/lib/rsmp/logger.rb CHANGED
@@ -96,7 +96,8 @@ module RSMP
96
96
  'statistics' => 'light_black',
97
97
  'not_acknowledged' => 'cyan',
98
98
  'warning' => 'light_yellow',
99
- 'error' => 'red'
99
+ 'error' => 'red',
100
+ 'debug' => 'light_black'
100
101
  }
101
102
  colors.merge! @settings["color"] if @settings["color"].is_a?(Hash)
102
103
  if colors[level.to_s]
@@ -0,0 +1,183 @@
1
+ module RSMP
2
+
3
+ # Base class for waiting for specific status or command responses, specified by
4
+ # a list of queries. Queries are defined as an array of hashes, e.g
5
+ # [
6
+ # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"securityCode", "v"=>"1111"},
7
+ # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"year", "v"=>"2020"},
8
+ # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>/\d+/}
9
+ # ]
10
+ #
11
+ # Note that queries can contain regex patterns for values, like /\d+/ in the example above.
12
+ #
13
+ # When an input messages is received it typically contains several items, eg:
14
+ # [
15
+ # {"cCI"=>"M0104", "n"=>"month", "v"=>"9", "age"=>"recent"},
16
+ # {"cCI"=>"M0104", "n"=>"day", "v"=>"29", "age"=>"recent"},
17
+ # {"cCI"=>"M0104", "n"=>"hour", "v"=>"17", "age"=>"recent"}
18
+ # ]
19
+ #
20
+ # Each input item is matched against each of the queries.
21
+ # If a match is found, it's stored in the @results hash, with the query as the key,
22
+ # and a mesage and status as the key. In the example above, this query:
23
+ #
24
+ # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>/\d+/}
25
+ #
26
+ # matches this input:
27
+ #
28
+ # {"cCI"=>"M0104", "n"=>"month", "v"=>"9", "age"=>"recent"}
29
+ #
30
+ # And the result is stored as:
31
+ # {
32
+ # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>/\d+/} =>
33
+ # { <StatusResponse message>, {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>"9"} }
34
+ # }
35
+ #
36
+ #
37
+ class Matcher < Collector
38
+
39
+ # Initialize with a list a wanted statuses
40
+ def initialize proxy, want, options={}
41
+ super proxy, options.merge( ingoing: true, outgoing: false)
42
+ @queries = {}
43
+ want.each do |query|
44
+ @queries[query] = nil
45
+ end
46
+ end
47
+
48
+ # Get the results, as a hash of queries => results
49
+ def result
50
+ @queries
51
+ end
52
+
53
+ # Get messages from results
54
+ def messages
55
+ @queries.map { |query,result| result[:message] }.uniq
56
+ end
57
+
58
+ # get items from results
59
+ def items
60
+ @queries.map { |query,result| result[:item] }.uniq
61
+ end
62
+
63
+ # Queries left to match?
64
+ def done?
65
+ @queries.values.all? { |result| result != nil }
66
+ end
67
+
68
+ # Mark a query as matched, by linking it to the matched item and message
69
+ def keep query, message, item
70
+ @queries[query] = { message:message, item:item }
71
+ end
72
+
73
+ # Mark a query as not matched
74
+ def forget query
75
+ @queries[query] = nil
76
+ end
77
+
78
+ # Check if a messages is wanted.
79
+ # Returns true when we found all that we want.
80
+ def check_match message
81
+ return unless match?(message)
82
+ @queries.keys.each do |query| # look through queries
83
+ get_items(message).each do |item| # look through status items in message
84
+ break if check_item_match message, query, item
85
+ end
86
+ end
87
+ end
88
+
89
+ # Check if an item matches, and mark query as matched/unmatched accordingly.
90
+ def check_item_match message, query, item
91
+ matched = match_item? query, item
92
+ if matched == true
93
+ keep query, message, item
94
+ true
95
+ elsif matched == false
96
+ forget query
97
+ true
98
+ end
99
+ end
100
+ end
101
+
102
+ # Class for waiting for specific command responses
103
+ class CommandResponseMatcher < Matcher
104
+ def initialize proxy, want, options={}
105
+ super proxy, want, options.merge(
106
+ type: ['CommandResponse','MessageNotAck'],
107
+ title:'command request'
108
+ )
109
+ end
110
+
111
+ def get_items message
112
+ message.attributes['rvs']
113
+ end
114
+
115
+ # Match an item against a query
116
+ def match_item? query, item
117
+ return nil if query['cCI'] && query['cCI'] != item['cCI']
118
+ return nil if query['n'] && query['n'] != item['n']
119
+ if query['v'].is_a? Regexp
120
+ return false if query['v'] && item['v'] !~ query['v']
121
+ else
122
+ return false if query['v'] && item['v'] != query['v']
123
+ end
124
+ true
125
+ end
126
+ end
127
+
128
+ # Base class for waiting for status updates or responses
129
+ class StatusUpdateOrResponseMatcher < Matcher
130
+ def initialize proxy, want, options={}
131
+ super proxy, want, options.merge
132
+ end
133
+
134
+ def get_items message
135
+ message.attributes['sS']
136
+ end
137
+
138
+ # Match an item against a query
139
+ def match_item? query, item
140
+ return nil if query['sCI'] && query['sCI'] != item['sCI']
141
+ return nil if query['cO'] && query['cO'] != item['cO']
142
+ return nil if query['n'] && query['n'] != item['n']
143
+ return false if query['q'] && query['q'] != item['q']
144
+ if query['s'].is_a? Regexp
145
+ return false if query['s'] && item['s'] !~ query['s']
146
+ else
147
+ return false if query['s'] && item['s'] != query['s']
148
+ end
149
+ true
150
+ end
151
+ end
152
+
153
+ # Class for waiting for specific status responses
154
+ class StatusResponseMatcher < StatusUpdateOrResponseMatcher
155
+ def initialize proxy, want, options={}
156
+ super proxy, want, options.merge(
157
+ type: ['StatusResponse','MessageNotAck'],
158
+ title: 'status request'
159
+ )
160
+ end
161
+ end
162
+
163
+ # Class for waiting for specific status responses
164
+ class StatusUpdateMatcher < StatusUpdateOrResponseMatcher
165
+ def initialize proxy, want, options={}
166
+ super proxy, want, options.merge(
167
+ type: ['StatusUpdate','MessageNotAck'],
168
+ title:'status subscription'
169
+ )
170
+ end
171
+ end
172
+
173
+ # Class for waiting for an aggregated status response
174
+ class AggregatedStatusMatcher < Collector
175
+ def initialize proxy, options={}
176
+ super proxy, options.merge(
177
+ num: 1,
178
+ type: ['AggregatedStatus','MessageNotAck'],
179
+ title: 'aggregated status request'
180
+ )
181
+ end
182
+ end
183
+ end
data/lib/rsmp/proxy.rb CHANGED
@@ -11,22 +11,32 @@ module RSMP
11
11
  include Notifier
12
12
  include Inspect
13
13
 
14
- attr_reader :state, :archive, :connection_info, :sxl, :task, :collector
14
+ attr_reader :state, :archive, :connection_info, :sxl, :task, :collector, :ip, :port
15
15
 
16
16
  def initialize options
17
17
  initialize_logging options
18
+ setup options
19
+ initialize_distributor
20
+ prepare_collection @settings['collect']
21
+ clear
22
+ end
23
+
24
+ def revive options
25
+ setup options
26
+ end
27
+
28
+ def setup options
18
29
  @settings = options[:settings]
19
30
  @task = options[:task]
20
31
  @socket = options[:socket]
32
+ @stream = options[:stream]
33
+ @protocol = options[:protocol]
21
34
  @ip = options[:ip]
22
35
  @port = options[:port]
23
36
  @connection_info = options[:info]
24
37
  @sxl = nil
25
38
  @site_settings = nil # can't pick until we know the site id
26
- initialize_distributor
27
-
28
- prepare_collection @settings['collect']
29
- clear
39
+ @state = :stopped
30
40
  end
31
41
 
32
42
  def inspect
@@ -49,6 +59,7 @@ module RSMP
49
59
  def collect task, options, &block
50
60
  collector = RSMP::Collector.new self, options
51
61
  collector.collect task, &block
62
+ collector
52
63
  end
53
64
 
54
65
  def run
@@ -105,8 +116,8 @@ module RSMP
105
116
  def start_reader
106
117
  @reader = @task.async do |task|
107
118
  task.annotate "reader"
108
- @stream = Async::IO::Stream.new(@socket)
109
- @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
119
+ @stream ||= Async::IO::Stream.new(@socket)
120
+ @protocol ||= Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
110
121
 
111
122
  while json = @protocol.read_line
112
123
  beginning = Time.now
@@ -3,7 +3,6 @@
3
3
  module RSMP
4
4
  class SiteProxy < Proxy
5
5
  include Components
6
- include SiteProxyWait
7
6
 
8
7
  attr_reader :supervisor, :site_id
9
8
 
@@ -15,6 +14,13 @@ module RSMP
15
14
  @site_id = nil
16
15
  end
17
16
 
17
+ def revive options
18
+ super options
19
+ @supervisor = options[:supervisor]
20
+ @settings = @supervisor.supervisor_settings.clone
21
+ end
22
+
23
+
18
24
  def inspect
19
25
  "#<#{self.class.name}:#{self.object_id}, #{inspector(
20
26
  :@acknowledgements,:@settings,:@site_settings,:@components
@@ -36,7 +42,7 @@ module RSMP
36
42
 
37
43
  def connection_complete
38
44
  super
39
- sanitized_sxl_version = RSMP::Schemer.sanitize_version(@site_sxl_version)
45
+ sanitized_sxl_version = RSMP::Schemer.sanitize_version(@site_sxl_version)
40
46
  log "Connection to site #{@site_id} established, using core #{@rsmp_version}, #{@sxl} #{sanitized_sxl_version}", level: :info
41
47
  end
42
48
 
@@ -79,11 +85,14 @@ module RSMP
79
85
  acknowledge message
80
86
  send_version @site_id, rsmp_versions
81
87
  @version_determined = true
88
+ end
82
89
 
90
+ def validate_ready action
91
+ raise NotReady.new("Can't #{action} because connection is not ready. (Currently #{@state})") unless ready?
83
92
  end
84
93
 
85
94
  def request_aggregated_status component, options={}
86
- raise NotReady unless ready?
95
+ validate_ready 'request aggregated status'
87
96
  m_id = options[:m_id] || RSMP::Message.make_m_id
88
97
 
89
98
  message = RSMP::AggregatedStatusRequest.new({
@@ -93,9 +102,8 @@ module RSMP
93
102
  "mId" => m_id
94
103
  })
95
104
  if options[:collect]
96
- result = nil
97
105
  task = @task.async do |task|
98
- wait_for_aggregated_status task, options[:collect], m_id
106
+ wait_for_aggregated_status task, options[:collect].merge(m_id: m_id)
99
107
  end
100
108
  send_message message, validate: options[:validate]
101
109
  return message, task.wait
@@ -163,11 +171,11 @@ module RSMP
163
171
  end
164
172
 
165
173
  def request_status component, status_list, options={}
166
- raise NotReady unless ready?
174
+ validate_ready 'request status'
167
175
  m_id = options[:m_id] || RSMP::Message.make_m_id
168
176
 
169
177
  # additional items can be used when verifying the response,
170
- # but must to remove from the request
178
+ # but must be removed from the request
171
179
  request_list = status_list.map { |item| item.slice('sCI','n') }
172
180
 
173
181
  message = RSMP::StatusRequest.new({
@@ -177,23 +185,8 @@ module RSMP
177
185
  "sS" => request_list,
178
186
  "mId" => m_id
179
187
  })
180
- if options[:collect]
181
- result = nil
182
- task = @task.async do |task|
183
- collect_options = options[:collect].merge status_list: status_list
184
- collect_status_responses task, collect_options, m_id
185
- end
186
- send_message message, validate: options[:validate]
187
-
188
- # task.wait return the result of the task. if the task raised an exception
189
- # it will be reraised. but that mechanish does not work if multiple values
190
- # are returned. so manually raise if first element is an exception
191
- result = task.wait
192
- raise result.first if result.first.is_a? Exception
193
- return message, *result
194
- else
195
- send_message message, validate: options[:validate]
196
- message
188
+ send_while_collecting message, options do |task|
189
+ collect_status_responses task, status_list, options[:collect].merge(m_id: m_id)
197
190
  end
198
191
  end
199
192
 
@@ -202,8 +195,15 @@ module RSMP
202
195
  acknowledge message
203
196
  end
204
197
 
198
+ def send_while_collecting message, options, &block
199
+ task = @task.async { |task| yield task } if options[:collect]
200
+ send_message message, validate: options[:validate]
201
+ return message, task.wait if task
202
+ message
203
+ end
204
+
205
205
  def subscribe_to_status component, status_list, options={}
206
- raise NotReady unless ready?
206
+ validate_ready 'subscribe to status'
207
207
  m_id = options[:m_id] || RSMP::Message.make_m_id
208
208
 
209
209
  # additional items can be used when verifying the response,
@@ -217,28 +217,13 @@ module RSMP
217
217
  "sS" => subscribe_list,
218
218
  'mId' => m_id
219
219
  })
220
- if options[:collect]
221
- result = nil
222
- task = @task.async do |task|
223
- collect_options = options[:collect].merge status_list: status_list
224
- collect_status_updates task, collect_options, m_id
225
- end
226
- send_message message, validate: options[:validate]
227
-
228
- # task.wait return the result of the task. if the task raised an exception
229
- # it will be reraised. but that mechanish does not work if multiple values
230
- # are returned. so manually raise if first element is an exception
231
- result = task.wait
232
- raise result.first if result.first.is_a? Exception
233
- return message, *result
234
- else
235
- send_message message, validate: options[:validate]
236
- message
220
+ send_while_collecting message, options do |task|
221
+ collect_status_updates task, status_list, options[:collect].merge(m_id: m_id)
237
222
  end
238
223
  end
239
224
 
240
225
  def unsubscribe_to_status component, status_list, options={}
241
- raise NotReady unless ready?
226
+ validate_ready 'unsubscribe to status'
242
227
  message = RSMP::StatusUnsubscribe.new({
243
228
  "ntsOId" => '',
244
229
  "xNId" => '',
@@ -269,7 +254,7 @@ module RSMP
269
254
  end
270
255
 
271
256
  def send_command component, command_list, options={}
272
- raise NotReady unless ready?
257
+ validate_ready 'send command'
273
258
  m_id = options[:m_id] || RSMP::Message.make_m_id
274
259
  message = RSMP::CommandRequest.new({
275
260
  "ntsOId" => '',
@@ -278,23 +263,8 @@ module RSMP
278
263
  "arg" => command_list,
279
264
  "mId" => m_id
280
265
  })
281
- if options[:collect]
282
- result = nil
283
- task = @task.async do |task|
284
- collect_options = options[:collect].merge command_list: command_list
285
- collect_command_responses task, collect_options, m_id
286
- end
287
- send_message message, validate: options[:validate]
288
-
289
- # task.wait return the result of the task. if the task raised an exception
290
- # it will be reraised. but that mechanish does not work if multiple values
291
- # are returned. so manually raise if first element is an exception
292
- result = task.wait
293
- raise result.first if result.first.is_a? Exception
294
- return message, *result
295
- else
296
- send_message message, validate: options[:validate]
297
- message
266
+ send_while_collecting message, options do |task|
267
+ collect_command_responses task, command_list, options[:collect].merge(m_id: m_id)
298
268
  end
299
269
  end
300
270
 
@@ -370,5 +340,37 @@ module RSMP
370
340
  @supervisor.notify_error e, options if @supervisor
371
341
  end
372
342
 
343
+ def wait_for_alarm parent_task, options={}
344
+ matching_alarm = nil
345
+ message = collect(parent_task,options.merge(type: "Alarm", with_message: true, num: 1)) do |message|
346
+ # TODO check components
347
+ matching_alarm = nil
348
+ alarm = message
349
+ next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
350
+ next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
351
+ next if options[:aS] && options[:aS] != alarm.attribute("aS")
352
+ matching_alarm = alarm
353
+ break
354
+ end
355
+ if item
356
+ { message: message, status: matching_alarm }
357
+ end
358
+ end
359
+
360
+ def collect_status_updates task, status_list, options
361
+ StatusUpdateMatcher.new(self, status_list, options).collect task
362
+ end
363
+
364
+ def collect_status_responses task, status_list, options
365
+ StatusResponseMatcher.new(self, status_list, options).collect task
366
+ end
367
+
368
+ def collect_command_responses task, command_list, options
369
+ CommandResponseMatcher.new(self, command_list, options).collect task
370
+ end
371
+
372
+ def wait_for_aggregated_status task, options
373
+ AggregatedStatusMatcher.new(self, options).collect task
374
+ end
373
375
  end
374
376
  end
@@ -1,171 +0,0 @@
1
- # waiting for various types of messages and reponses from remote sites
2
- module RSMP
3
- module SiteProxyWait
4
-
5
- def wait_for_alarm parent_task, options={}
6
- matching_alarm = nil
7
- message = collect(parent_task,options.merge(type: "Alarm", with_message: true, num: 1)) do |message|
8
- # TODO check components
9
- matching_alarm = nil
10
- alarm = message
11
- next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
12
- next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
13
- next if options[:aS] && options[:aS] != alarm.attribute("aS")
14
- matching_alarm = alarm
15
- break
16
- end
17
- if item
18
- { message: message, status: matching_alarm }
19
- end
20
- end
21
-
22
- def collect_status_updates task, options, m_id
23
- collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
24
- end
25
-
26
- def collect_status_responses task, options, m_id
27
- collect_status_updates_or_responses task, 'StatusResponse', options, m_id
28
- end
29
-
30
- def collect_command_responses parent_task, options, m_id
31
- task.annotate "wait for command response"
32
- want = options[:command_list].clone
33
- result = {}
34
- messages = []
35
- collect(parent_task,options.merge({
36
- type: ['CommandResponse','MessageNotAck'],
37
- num: 1
38
- })) do |message|
39
- if message.is_a?(MessageNotAck)
40
- if message.attribute('oMId') == m_id
41
- # set result to an exception, but don't raise it.
42
- # this will be returned by the task and stored as the task result
43
- # when the parent task call wait() on the task, the exception
44
- # will be raised in the parent task, and caught by rspec.
45
- # rspec will then show the error and record the test as failed
46
- m_id_short = RSMP::Message.shorten_m_id m_id, 8
47
- result = RSMP::MessageRejected.new "Command request #{m_id_short} was rejected: #{message.attribute('rea')}"
48
- next true # done, no more messages wanted
49
- else
50
- false
51
- end
52
- else
53
- add = false
54
- # look through querues
55
- want.each_with_index do |query,i|
56
- # look through items in message
57
- message.attributes['rvs'].each do |input|
58
- matching = command_match? query, input
59
- if matching == true
60
- result[query] = input
61
- add = true
62
- elsif matching == false
63
- result.delete query
64
- end
65
- end
66
- end
67
- messages << message if add
68
- result.size == want.size # any queries left to match?
69
- end
70
- end
71
- return result, messages
72
- rescue Async::TimeoutError
73
- raise RSMP::TimeoutError.new "Did not receive correct command response to #{m_id} within #{options[:timeout]}s"
74
- end
75
-
76
- def collect_status_updates_or_responses task, type, options, m_id
77
- want = options[:status_list].clone
78
- result = {}
79
- messages = []
80
- # wait for a status update
81
- collect(task,options.merge({
82
- type: [type,'MessageNotAck'],
83
- num: 1
84
- })) do |message|
85
- if message.is_a?(MessageNotAck)
86
- if message.attribute('oMId') == m_id
87
- # set result to an exception, but don't raise it.
88
- # this will be returned by the task and stored as the task result
89
- # when the parent task call wait() on the task, the exception
90
- # will be raised in the parent task, and caught by rspec.
91
- # rspec will then show the error and record the test as failed
92
- m_id_short = RSMP::Message.shorten_m_id m_id, 8
93
- result = RSMP::MessageRejected.new "Status request #{m_id_short} was rejected: #{message.attribute('rea')}"
94
- next true # done, no more messages wanted
95
- end
96
- false
97
- else
98
- found = []
99
- add = false
100
- # look through querues
101
- want.each_with_index do |query,i|
102
- # look through status items in message
103
- message.attributes['sS'].each do |input|
104
- matching = status_match? query, input
105
- if matching == true
106
- result[query] = input
107
- add = true
108
- elsif matching == false
109
- result.delete query
110
- end
111
- end
112
- end
113
- messages << message if add
114
- result.size == want.size # any queries left to match?
115
- end
116
- end
117
- return result, messages
118
- rescue Async::TimeoutError
119
- type_str = {'StatusUpdate'=>'update', 'StatusResponse'=>'response'}[type]
120
- raise RSMP::TimeoutError.new "Did not received correct status #{type_str} in reply to #{m_id} within #{options[:timeout]}s"
121
- end
122
-
123
- def status_match? query, item
124
- return nil if query['sCI'] && query['sCI'] != item['sCI']
125
- return nil if query['n'] && query['n'] != item['n']
126
- return false if query['q'] && query['q'] != item['q']
127
- if query['s'].is_a? Regexp
128
- return false if query['s'] && item['s'] !~ query['s']
129
- else
130
- return false if query['s'] && item['s'] != query['s']
131
- end
132
- true
133
- end
134
-
135
- def command_match? query, item
136
- return nil if query['cCI'] && query['cCI'] != item['cCI']
137
- return nil if query['n'] && query['n'] != item['n']
138
- if query['v'].is_a? Regexp
139
- return false if query['v'] && item['v'] !~ query['v']
140
- else
141
- return false if query['v'] && item['v'] != query['v']
142
- end
143
- true
144
- end
145
-
146
- def wait_for_aggregated_status parent_task, options, m_id
147
- collect(parent_task,options.merge({
148
- type: ['AggregatedStatus','MessageNotAck'],
149
- num: 1
150
- })) do |message|
151
- if message.is_a?(MessageNotAck)
152
- if message.attribute('oMId') == m_id
153
- # set result to an exception, but don't raise it.
154
- # this will be returned by the task and stored as the task result
155
- # when the parent task call wait() on the task, the exception
156
- # will be raised in the parent task, and caught by rspec.
157
- # rspec will then show the error and record the test as failed
158
- m_id_short = RSMP::Message.shorten_m_id m_id, 8
159
- result = RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected: #{message.attribute('rea')}"
160
- next true # done, no more messages wanted
161
- else
162
- false
163
- end
164
- else
165
- true
166
- end
167
- end
168
- end
169
-
170
- end
171
- end
@@ -136,6 +136,13 @@ module RSMP
136
136
  end
137
137
  end
138
138
 
139
+ def peek_version_message protocol
140
+ json = protocol.peek_line
141
+ attributes = Message.parse_attributes json
142
+ message = Message.build attributes, json
143
+ message.attribute('siteId').first['sId']
144
+ end
145
+
139
146
  def connect socket, info
140
147
  log "Site connected from #{format_ip_and_port(info)}",
141
148
  ip: info[:ip],
@@ -144,25 +151,36 @@ module RSMP
144
151
  timestamp: Clock.now
145
152
 
146
153
  authorize_ip info[:ip]
147
- check_max_sites
148
154
 
149
- proxy = build_proxy({
155
+ stream = Async::IO::Stream.new socket
156
+ protocol = Async::IO::Protocol::Line.new stream, Proxy::WRAPPING_DELIMITER
157
+
158
+ settings = {
150
159
  supervisor: self,
151
160
  ip: info[:ip],
152
161
  port: info[:port],
153
162
  task: @task,
154
163
  settings: {'collect'=>@supervisor_settings['collect']},
155
164
  socket: socket,
165
+ stream: stream,
166
+ protocol: protocol,
156
167
  info: info,
157
168
  logger: @logger,
158
169
  archive: @archive
159
- })
160
- @proxies.push proxy
170
+ }
171
+
172
+ id = peek_version_message protocol
173
+ proxy = find_site id
174
+ if proxy
175
+ proxy.revive settings
176
+ else
177
+ check_max_sites
178
+ proxy = build_proxy settings
179
+ @proxies.push proxy
180
+ end
161
181
  proxy.run # will run until the site disconnects
162
182
  ensure
163
- @proxies.delete proxy
164
183
  site_ids_changed
165
-
166
184
  stop if @supervisor_settings['one_shot']
167
185
  end
168
186
 
@@ -188,7 +206,14 @@ module RSMP
188
206
  return find_site(site_id) != nil
189
207
  end
190
208
 
191
- def find_site site_id
209
+ def find_site_from_ip_port ip, port
210
+ @proxies.each do |site|
211
+ return site if site.ip == ip && site.port == port
212
+ end
213
+ nil
214
+ end
215
+
216
+ def find_site site_id
192
217
  @proxies.each do |site|
193
218
  return site if site_id == :any || site.site_id == site_id
194
219
  end
@@ -200,7 +225,12 @@ module RSMP
200
225
  return site if site
201
226
  wait_for(@site_id_condition,timeout) { find_site site_id }
202
227
  rescue Async::TimeoutError
203
- raise RSMP::TimeoutError.new "Site '#{site_id}' did not connect within #{timeout}s"
228
+ if site_id == :any
229
+ str = "No site connected"
230
+ else
231
+ str = "Site '#{site_id}' did not connect"
232
+ end
233
+ raise RSMP::TimeoutError.new "#{str} within #{timeout}s"
204
234
  end
205
235
 
206
236
  def wait_for_site_disconnect site_id, timeout
@@ -210,12 +240,13 @@ module RSMP
210
240
  end
211
241
 
212
242
  def check_site_id site_id
213
- check_site_already_connected site_id
243
+ #check_site_already_connected site_id
214
244
  return site_id_to_site_setting site_id
215
245
  end
216
246
 
217
247
  def check_site_already_connected site_id
218
- raise FatalError.new "Site '#{site_id}' already connected" if find_site(site_id)
248
+ site = find_site(site_id)
249
+ raise FatalError.new "Site '#{site_id}' already connected" if site != nil && site != self
219
250
  end
220
251
 
221
252
  def site_id_to_site_setting site_id
@@ -129,7 +129,7 @@ module RSMP
129
129
  message = AggregatedStatus.new({
130
130
  "aSTS" => clock.to_s,
131
131
  "cId" => component.c_id,
132
- "fP" => 'NormalControl',
132
+ "fP" => nil,
133
133
  "fS" => nil,
134
134
  "se" => component.aggregated_status_bools,
135
135
  "mId" => m_id,
@@ -146,15 +146,6 @@ module RSMP
146
146
  send_message message, validate: options[:validate]
147
147
  message
148
148
  end
149
-
150
- # send an invalid message
151
- message = AggregatedStatus.new({
152
- "aSTS" => clock.to_s,
153
- "cId" => component.c_id,
154
- "fP" => 'Invalid',
155
- })
156
- send_message message, validate: false
157
-
158
149
  end
159
150
 
160
151
  def process_aggregated_status message
data/lib/rsmp/tlc.rb CHANGED
@@ -798,7 +798,7 @@ module RSMP
798
798
  super options
799
799
  @sxl = 'traffic_light_controller'
800
800
  @security_codes = options[:site_settings]['security_codes']
801
- @interval = options[:site_settings]['intervals']['timer'] || 1
801
+ @interval = options[:site_settings].dig('intervals','timer') || 1
802
802
  unless @main
803
803
  raise ConfigurationError.new "TLC must have a main component"
804
804
  end
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = "0.2"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -21,11 +21,11 @@ require 'rsmp/notifier'
21
21
 
22
22
  require 'rsmp/listener'
23
23
  require 'rsmp/collector'
24
+ require 'rsmp/matcher'
24
25
  require 'rsmp/component'
25
26
  require 'rsmp/site'
26
27
  require 'rsmp/proxy'
27
28
  require 'rsmp/supervisor_proxy'
28
- require 'rsmp/site_proxy_wait'
29
29
  require 'rsmp/site_proxy'
30
30
  require 'rsmp/error'
31
31
  require 'rsmp/message'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsmp
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-02 00:00:00.000000000 Z
11
+ date: 2021-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -217,6 +217,7 @@ files:
217
217
  - lib/rsmp/listener.rb
218
218
  - lib/rsmp/logger.rb
219
219
  - lib/rsmp/logging.rb
220
+ - lib/rsmp/matcher.rb
220
221
  - lib/rsmp/message.rb
221
222
  - lib/rsmp/node.rb
222
223
  - lib/rsmp/notifier.rb
@@ -254,7 +255,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
255
  - !ruby/object:Gem::Version
255
256
  version: '0'
256
257
  requirements: []
257
- rubygems_version: 3.2.15
258
+ rubygems_version: 3.2.26
258
259
  signing_key:
259
260
  specification_version: 4
260
261
  summary: RoadSide Message Protocol (RSMP) library.