rsmp 0.2.1 → 0.3.1

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: 43f928133404e5dbc56e1a45eba044b20783bd2decfd1631a0ae2b66e731e5e2
4
- data.tar.gz: ad786c056a21aa65a65406edfe3200229a5b02a104871e63e656d4dae4c6910c
3
+ metadata.gz: a19b3e748ae4dff7c8f652f7015d8f0b798f78a3d8f07c545638724308f4c819
4
+ data.tar.gz: 8353a6b756fa36f45b0c9e8cf33d56cb96f69ef30ab158ebf34ea2cc8a863122
5
5
  SHA512:
6
- metadata.gz: 453a4f85fbd4985dc300fd6321c2c534723b251410e6a7c577aff53175197aa1e8898234a1086b1d747fe4e093a67007b6ffd426e15a1b0d3a6ebaa489791f2a
7
- data.tar.gz: 7c443d27d2dbc108b5b34af6faf15fa03e145193f83584c465ae685f74de4203db0cd37d06fdee4525a06077cd0a208020e7f9a635b021d20eb92050eeaaa8d5
6
+ metadata.gz: 23fb75e6bad681f67be2f04038a6bebec458c5e6e5c29ee5d5333dc5583b490ba8e1841e5304a88a384ac4761edea45818848f2144a97e73a31dbe2ca89090ed
7
+ data.tar.gz: cc37d48a622961d41444631cb3543967ccbd926dfcf1314a45294c676270977272e842853a5e1f59a08f1706b26dd85a2494aaea1a12b5dced4917dbaaee9015
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.2.1)
4
+ rsmp (0.3.1)
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,20 @@
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
+ @options[:num] ||= 1
17
+ reset
19
18
  end
20
19
 
21
20
  def inspect
@@ -34,17 +33,9 @@ module RSMP
34
33
  @condition.wait
35
34
  end
36
35
 
37
- def collect_for task, duration
38
- siphon do
39
- task.sleep duration
40
- end
41
- end
42
-
43
36
  def collect task, options={}, &block
44
- @num = options[:num] if options[:num]
45
- @options[:timeout] = options[:timeout] if options[:timeout]
37
+ @options.merge! options
46
38
  @block = block
47
-
48
39
  unless @done
49
40
  listen do
50
41
  task.with_timeout(@options[:timeout]) do
@@ -52,51 +43,105 @@ module RSMP
52
43
  end
53
44
  end
54
45
  end
46
+ return @error if @error
47
+ self
48
+ rescue Async::TimeoutError
49
+ str = "Did not receive #{@title}"
50
+ str << " in response to #{options[:m_id]}" if options[:m_id]
51
+ str << " within #{@options[:timeout]}s"
52
+ raise RSMP::TimeoutError.new str
53
+ end
55
54
 
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
55
+ # Get the collected messages.
56
+ # If one message was requested, return it as a plain object instead of array
57
+ def result
58
+ return @messages.first if @options[:num] == 1
59
+ @messages.first @options[:num]
62
60
  end
63
61
 
62
+ # Clear all query results
64
63
  def reset
65
- @message.clear
64
+ @messages = []
65
+ @error = nil
66
66
  @done = false
67
67
  end
68
68
 
69
+ # Check if we receive a NotAck related to initiating request, identified by @m_id.
70
+ def check_not_ack message
71
+ return unless @options[:m_id]
72
+ if message.is_a?(MessageNotAck)
73
+ if message.attribute('oMId') == @options[:m_id]
74
+ m_id_short = RSMP::Message.shorten_m_id @options[:m_id], 8
75
+ @error = RSMP::MessageRejected.new("#{@title} #{m_id_short} was rejected: #{message.attribute('rea')}")
76
+ complete
77
+ end
78
+ false
79
+ end
80
+ end
81
+
82
+ # Handle message. and return true when we're done collecting
69
83
  def notify message
70
84
  raise ArgumentError unless message
85
+ raise RuntimeError.new("can't process message when already done") if @done
86
+ check_not_ack(message)
71
87
  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
88
+ check_match message
89
+ complete if done?
90
+ @done
91
+ end
92
+
93
+ # Match message against our collection criteria
94
+ def check_match message
95
+ matched = match? message
96
+ if matched == true
97
+ keep message
98
+ elsif matched == false
99
+ forget message
81
100
  end
82
101
  end
83
102
 
84
- def matches? message
85
- raise ArgumentError unless message
103
+ # Have we collected the required number of messages?
104
+ def done?
105
+ @options[:num] && @messages.size >= @options[:num]
106
+ end
107
+
108
+ # Called when we're done collecting. Remove ourself as a listener,
109
+ # se we don't receive message notifications anymore
110
+ def complete
111
+ @done = true
112
+ @proxy.remove_listener self
113
+ @condition.signal
114
+ end
115
+
116
+ # Store a message in the result array
117
+ def keep message
118
+ @messages << message
119
+ end
120
+
121
+ # Remove a message from the result array
122
+ def forget message
123
+ @messages.delete message
124
+ end
86
125
 
126
+ # Check a message against our match criteria
127
+ # Return true if there's a match
128
+ def match? message
129
+ raise ArgumentError unless message
130
+ return if message.direction == :in && @ingoing == false
131
+ return if message.direction == :out && @outgoing == false
87
132
  if @options[:type]
88
- return false if message == nil
133
+ return if message == nil
89
134
  if @options[:type].is_a? Array
90
- return false unless @options[:type].include? message.type
135
+ return unless @options[:type].include? message.type
91
136
  else
92
- return false unless message.type == @options[:type]
137
+ return unless message.type == @options[:type]
93
138
  end
94
139
  end
95
140
  if @options[:component]
96
- return false if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
141
+ return if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
97
142
  end
98
143
  if @block
99
- return false if @block.call(message) == false
144
+ return if @block.call(message) == false
100
145
  end
101
146
  true
102
147
  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,195 @@
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
+ attr_reader :queries
39
+
40
+ # Initialize with a list a wanted statuses
41
+ def initialize proxy, want, options={}
42
+ super proxy, options.merge( ingoing: true, outgoing: false)
43
+ @queries = {}
44
+ want.each do |query|
45
+ @queries[query] = nil
46
+ end
47
+ end
48
+
49
+ # Get the results, as a hash of queries => results
50
+ def result
51
+ @queries
52
+ end
53
+
54
+ # Get messages from results
55
+ def messages
56
+ @queries.map { |query,result| result[:message] }.uniq
57
+ end
58
+
59
+ # Get items from results
60
+ def items
61
+ @queries.map { |query,result| result[:item] }.uniq
62
+ end
63
+
64
+ # Are there queries left to match?
65
+ def done?
66
+ @queries.values.all? { |result| result != nil }
67
+ end
68
+
69
+ # Get a simplified hash of queries, with values set to either true or false,
70
+ # indicating which queries have been matched.
71
+ def status
72
+ @queries.transform_values{ |v| v != nil }
73
+ end
74
+
75
+ # Get a simply array of bools, showing which queries ahve been matched.
76
+ def summary
77
+ @queries.values.map { |v| v != nil }
78
+ end
79
+
80
+ # Mark a query as matched, by linking it to the matched item and message
81
+ def keep query, message, item
82
+ @queries[query] = { message:message, item:item }
83
+ end
84
+
85
+ # Mark a query as not matched
86
+ def forget query
87
+ @queries[query] = nil
88
+ end
89
+
90
+ # Check if a messages matches our criteria.
91
+ # We iterate through each of the status items or return values in the message
92
+ # Breaks as soon as where done matching all queries
93
+ def check_match message
94
+ return unless match?(message)
95
+ @queries.keys.each do |query| # look through queries
96
+ get_items(message).each do |item| # look through status items in message
97
+ break if check_item_match message, query, item
98
+ end
99
+ end
100
+ end
101
+
102
+ # Check if an item matches, and mark query as matched/unmatched accordingly.
103
+ def check_item_match message, query, item
104
+ matched = match_item? query, item
105
+ if matched == true
106
+ keep query, message, item
107
+ true
108
+ elsif matched == false
109
+ forget query
110
+ true
111
+ end
112
+ end
113
+ end
114
+
115
+ # Class for waiting for specific command responses
116
+ class CommandResponseMatcher < Matcher
117
+ def initialize proxy, want, options={}
118
+ super proxy, want, options.merge(
119
+ type: ['CommandResponse','MessageNotAck'],
120
+ title:'command response'
121
+ )
122
+ end
123
+
124
+ # Get items, in our case the return values
125
+ def get_items message
126
+ message.attributes['rvs']
127
+ end
128
+
129
+ # Match a return value item against a query
130
+ def match_item? query, item
131
+ return nil if query['cCI'] && query['cCI'] != item['cCI']
132
+ return nil if query['n'] && query['n'] != item['n']
133
+ if query['v'].is_a? Regexp
134
+ return false if query['v'] && item['v'] !~ query['v']
135
+ else
136
+ return false if query['v'] && item['v'] != query['v']
137
+ end
138
+ true
139
+ end
140
+ end
141
+
142
+ # Base class for waiting for status updates or responses
143
+ class StatusUpdateOrResponseMatcher < Matcher
144
+ def initialize proxy, want, options={}
145
+ super proxy, want, options.merge
146
+ end
147
+
148
+ # Get items, in our case status values
149
+ def get_items message
150
+ message.attributes['sS']
151
+ end
152
+
153
+ # Match a status value against a query
154
+ def match_item? query, item
155
+ return nil if query['sCI'] && query['sCI'] != item['sCI']
156
+ return nil if query['cO'] && query['cO'] != item['cO']
157
+ return nil if query['n'] && query['n'] != item['n']
158
+ return false if query['q'] && query['q'] != item['q']
159
+ if query['s'].is_a? Regexp
160
+ return false if query['s'] && item['s'] !~ query['s']
161
+ else
162
+ return false if query['s'] && item['s'] != query['s']
163
+ end
164
+ true
165
+ end
166
+ end
167
+
168
+ # Class for waiting for specific status responses
169
+ class StatusResponseMatcher < StatusUpdateOrResponseMatcher
170
+ def initialize proxy, want, options={}
171
+ super proxy, want, options.merge(
172
+ type: ['StatusResponse','MessageNotAck'],
173
+ title: 'status response'
174
+ )
175
+ end
176
+ end
177
+
178
+ # Class for waiting for specific status responses
179
+ class StatusUpdateMatcher < StatusUpdateOrResponseMatcher
180
+ def initialize proxy, want, options={}
181
+ super proxy, want, options.merge(
182
+ type: ['StatusUpdate','MessageNotAck'],
183
+ title:'status update'
184
+ )
185
+ end
186
+ end
187
+
188
+ # Class for waiting for an aggregated status response
189
+ class AggregatedStatusMatcher < Collector
190
+ def initialize proxy, options={}
191
+ required = { type: ['AggregatedStatus','MessageNotAck'], title: 'aggregated status' }
192
+ super proxy, options.merge(required)
193
+ end
194
+ end
195
+ 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,15 +116,19 @@ 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
113
124
  message = process_packet json
114
125
  duration = Time.now - beginning
115
126
  ms = (duration*1000).round(4)
116
- per_second = (1.0 / duration).round
127
+ if duration > 0
128
+ per_second = (1.0 / duration).round
129
+ else
130
+ per_second = Float::INFINITY
131
+ end
117
132
  if message
118
133
  type = message.type
119
134
  m_id = Logger.shorten_message_id(message.m_id)
@@ -192,7 +207,6 @@ module RSMP
192
207
  def watchdog_send_timer now
193
208
  return unless @watchdog_started
194
209
  return if @site_settings['intervals']['watchdog'] == :never
195
-
196
210
  if @latest_watchdog_send_at == nil
197
211
  send_watchdog now
198
212
  else
data/lib/rsmp/site.rb CHANGED
@@ -41,7 +41,9 @@ module RSMP
41
41
  },
42
42
  'send_after_connect' => true,
43
43
  'components' => {
44
- 'C1' => {}
44
+ 'main' => {
45
+ 'C1' => {}
46
+ }
45
47
  }
46
48
  }
47
49
 
@@ -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
 
@@ -12,9 +11,16 @@ module RSMP
12
11
  initialize_components
13
12
  @supervisor = options[:supervisor]
14
13
  @settings = @supervisor.supervisor_settings.clone
15
- @site_id = nil
14
+ @site_id = options[:site_id]
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
+ collect_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 collect_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.merge(site_id:id) # keep the id learned by peeking above
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,
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.1"
2
+ VERSION = "0.3.1"
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.1
4
+ version: 0.3.1
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-18 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