rsmp 0.1.19 → 0.1.31

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.
@@ -0,0 +1,206 @@
1
+ # waiting for various types of messages and reponses from remote sites
2
+ module RSMP
3
+ module SiteProxyWait
4
+
5
+ def wait_for_status_updates parent_task, options={}, &send_block
6
+ send_while_collecting parent_task, send_block do |task, m_id|
7
+ collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
8
+ end
9
+ end
10
+
11
+ def wait_for_status_responses parent_task, options={}, &send_block
12
+ send_while_collecting parent_task, send_block do |task, m_id|
13
+ collect_status_updates_or_responses task, 'StatusResponse', options, m_id
14
+ end
15
+ end
16
+
17
+ def wait_for_command_responses parent_task, options={}, &send_block
18
+ send_while_collecting parent_task, send_block do |task, m_id|
19
+ collect_command_responses task, options, m_id
20
+ end
21
+ end
22
+
23
+ def wait_for_alarm parent_task, options={}
24
+ matching_alarm = nil
25
+ message = collect(parent_task,options.merge(type: "Alarm", with_message: true, num: 1)) do |message|
26
+ # TODO check components
27
+ matching_alarm = nil
28
+ alarm = message
29
+ next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
30
+ next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
31
+ next if options[:aS] && options[:aS] != alarm.attribute("aS")
32
+ matching_alarm = alarm
33
+ break
34
+ end
35
+ if item
36
+ { message: message, status: matching_alarm }
37
+ end
38
+ end
39
+
40
+ def collect_status_updates task, options, m_id
41
+ collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
42
+ end
43
+
44
+ def collect_status_responses task, options, m_id
45
+ collect_status_updates_or_responses task, 'StatusResponse', options, m_id
46
+ end
47
+
48
+ def collect_command_responses parent_task, options, m_id
49
+ task.annotate "wait for command response"
50
+ want = options[:command_list].clone
51
+ result = {}
52
+ messages = []
53
+ collect(parent_task,options.merge({
54
+ type: ['CommandResponse','MessageNotAck'],
55
+ num: 1
56
+ })) do |message|
57
+ if message.is_a?(MessageNotAck)
58
+ if message.attribute('oMId') == m_id
59
+ # set result to an exception, but don't raise it.
60
+ # this will be returned by the task and stored as the task result
61
+ # when the parent task call wait() on the task, the exception
62
+ # will be raised in the parent task, and caught by rspec.
63
+ # rspec will then show the error and record the test as failed
64
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
65
+ result = RSMP::MessageRejected.new "Command request #{m_id_short} was rejected: #{message.attribute('rea')}"
66
+ next true # done, no more messages wanted
67
+ else
68
+ false
69
+ end
70
+ else
71
+ add = false
72
+ # look through querues
73
+ want.each_with_index do |query,i|
74
+ # look through items in message
75
+ message.attributes['rvs'].each do |input|
76
+ matching = command_match? query, input
77
+ if matching == true
78
+ result[query] = input
79
+ add = true
80
+ elsif matching == false
81
+ result.delete query
82
+ end
83
+ end
84
+ end
85
+ messages << message if add
86
+ result.size == want.size # any queries left to match?
87
+ end
88
+ end
89
+ return result, messages
90
+ rescue Async::TimeoutError
91
+ raise RSMP::TimeoutError.new "Did not receive correct command response to #{m_id} within #{options[:timeout]}s"
92
+ end
93
+
94
+ def collect_status_updates_or_responses task, type, options, m_id
95
+ want = options[:status_list].clone
96
+ result = {}
97
+ messages = []
98
+ # wait for a status update
99
+ collect(task,options.merge({
100
+ type: [type,'MessageNotAck'],
101
+ num: 1
102
+ })) do |message|
103
+ if message.is_a?(MessageNotAck)
104
+ if message.attribute('oMId') == m_id
105
+ # set result to an exception, but don't raise it.
106
+ # this will be returned by the task and stored as the task result
107
+ # when the parent task call wait() on the task, the exception
108
+ # will be raised in the parent task, and caught by rspec.
109
+ # rspec will then show the error and record the test as failed
110
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
111
+ result = RSMP::MessageRejected.new "Status request #{m_id_short} was rejected: #{message.attribute('rea')}"
112
+ next true # done, no more messages wanted
113
+ end
114
+ false
115
+ else
116
+ found = []
117
+ add = false
118
+ # look through querues
119
+ want.each_with_index do |query,i|
120
+ # look through status items in message
121
+ message.attributes['sS'].each do |input|
122
+ matching = status_match? query, input
123
+ if matching == true
124
+ result[query] = input
125
+ add = true
126
+ elsif matching == false
127
+ result.delete query
128
+ end
129
+ end
130
+ end
131
+ messages << message if add
132
+ result.size == want.size # any queries left to match?
133
+ end
134
+ end
135
+ return result, messages
136
+ rescue Async::TimeoutError
137
+ type_str = {'StatusUpdate'=>'update', 'StatusResponse'=>'response'}[type]
138
+ raise RSMP::TimeoutError.new "Did not received correct status #{type_str} in reply to #{m_id} within #{options[:timeout]}s"
139
+ end
140
+
141
+ def status_match? query, item
142
+ return nil if query['sCI'] && query['sCI'] != item['sCI']
143
+ return nil if query['n'] && query['n'] != item['n']
144
+ return false if query['q'] && query['q'] != item['q']
145
+ if query['s'].is_a? Regexp
146
+ return false if query['s'] && item['s'] !~ query['s']
147
+ else
148
+ return false if query['s'] && item['s'] != query['s']
149
+ end
150
+ true
151
+ end
152
+
153
+ def command_match? query, item
154
+ return nil if query['cCI'] && query['cCI'] != item['cCI']
155
+ return nil if query['n'] && query['n'] != item['n']
156
+ if query['v'].is_a? Regexp
157
+ return false if query['v'] && item['v'] !~ query['v']
158
+ else
159
+ return false if query['v'] && item['v'] != query['v']
160
+ end
161
+ true
162
+ end
163
+
164
+ def send_while_collecting parent_task, send_block, &collect_block
165
+ m_id = RSMP::Message.make_m_id # make message id so we can start waiting for it
166
+
167
+ # wait for command responses in an async task
168
+ task = parent_task.async do |task|
169
+ collect_block.call task, m_id
170
+ rescue StandardError => e
171
+ notify_error e, level: :internal
172
+ end
173
+
174
+ # call block, it should send command request using the given m_id
175
+ send_block.call m_id
176
+
177
+ # wait for the response and return it, raise exception if NotAck received, it it timed out
178
+ task.wait
179
+ end
180
+
181
+ def wait_for_aggregated_status parent_task, options={}
182
+ collect(parent_task,options.merge({
183
+ type: ['AggregatedStatus','MessageNotAck'],
184
+ num: 1
185
+ })) do |message|
186
+ if message.is_a?(MessageNotAck)
187
+ if message.attribute('oMId') == m_id
188
+ # set result to an exception, but don't raise it.
189
+ # this will be returned by the task and stored as the task result
190
+ # when the parent task call wait() on the task, the exception
191
+ # will be raised in the parent task, and caught by rspec.
192
+ # rspec will then show the error and record the test as failed
193
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
194
+ result = RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected: #{message.attribute('rea')}"
195
+ next true # done, no more messages wanted
196
+ else
197
+ false
198
+ end
199
+ else
200
+ true
201
+ end
202
+ end
203
+ end
204
+
205
+ end
206
+ end
@@ -17,47 +17,57 @@ module RSMP
17
17
  @supervisor_settings['site_id']
18
18
  end
19
19
 
20
- def handle_supervisor_settings options
21
- @supervisor_settings = {
20
+ def handle_supervisor_settings options={}
21
+ defaults = {
22
22
  'port' => 12111,
23
- 'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
24
- 'timer_interval' => 0.1,
25
- 'watchdog_interval' => 1,
26
- 'watchdog_timeout' => 2,
27
- 'acknowledgement_timeout' => 2,
28
- 'command_response_timeout' => 1,
29
- 'status_response_timeout' => 1,
30
- 'status_update_timeout' => 1,
31
- 'site_connect_timeout' => 2,
32
- 'site_ready_timeout' => 1,
33
- 'stop_after_first_session' => false,
34
- 'sites' => {
35
- :any => {}
23
+ 'ips' => 'all',
24
+ 'guest' => {
25
+ 'rsmp_versions' => 'all',
26
+ 'sxl' => 'tlc',
27
+ 'intervals' => {
28
+ 'timer' => 1,
29
+ 'watchdog' => 1
30
+ },
31
+ 'timeouts' => {
32
+ 'watchdog' => 2,
33
+ 'acknowledgement' => 2
34
+ }
36
35
  }
37
36
  }
38
-
39
- if options[:supervisor_settings]
40
- converted = options[:supervisor_settings].map { |k,v| [k.to_s,v] }.to_h #convert symbol keys to string keys
41
- converted.compact!
42
- @supervisor_settings.merge! converted
43
- end
44
-
45
- required = [:port, :rsmp_versions, :watchdog_interval, :watchdog_timeout,
46
- :acknowledgement_timeout, :command_response_timeout]
47
- check_required_settings @supervisor_settings, required
48
37
 
38
+ # merge options into defaults
39
+ @supervisor_settings = defaults.deep_merge(options[:supervisor_settings] || {})
49
40
  @rsmp_versions = @supervisor_settings["rsmp_versions"]
41
+ check_site_sxl_types
42
+ end
43
+
44
+ def check_site_sxl_types
45
+ sites = @supervisor_settings['sites'].clone || {}
46
+ sites['guest'] = @supervisor_settings['guest']
47
+ sites.each do |site_id,settings|
48
+ unless settings
49
+ raise RSMP::ConfigurationError.new("Configuration for site '#{site_id}' is empty")
50
+ end
51
+ sxl = settings['sxl']
52
+ sxl = 'tlc' unless sxl # temporary fix until configs are updated
53
+ unless sxl
54
+ raise RSMP::ConfigurationError.new("Configuration error for site '#{site_id}': No SXL specified")
55
+ end
56
+ RSMP::Schemer.find_schemas! sxl if sxl
57
+ rescue RSMP::Schemer::UnknownSchemaError => e
58
+ raise RSMP::ConfigurationError.new("Configuration error for site '#{site_id}': #{e}")
59
+ end
50
60
  end
51
61
 
52
62
  def start_action
53
63
  @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
54
- @endpoint.accept do |socket|
64
+ @endpoint.accept do |socket| # creates async tasks
55
65
  handle_connection(socket)
66
+ rescue StandardError => e
67
+ notify_error e, level: :internal
56
68
  end
57
- rescue SystemCallError => e # all ERRNO errors
58
- log "Exception: #{e.to_s}", level: :error
59
69
  rescue StandardError => e
60
- log ["Exception: #{e.inspect}",e.backtrace].flatten.join("\n"), level: :error
70
+ notify_error e, level: :internal
61
71
  end
62
72
 
63
73
  def stop
@@ -74,16 +84,18 @@ module RSMP
74
84
  remote_hostname = socket.remote_address.ip_address
75
85
  remote_ip = socket.remote_address.ip_address
76
86
 
77
- info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:RSMP.now_string()}
87
+ info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:Clock.now}
78
88
  if accept? socket, info
79
89
  connect socket, info
80
90
  else
81
91
  reject socket, info
82
92
  end
83
- rescue SystemCallError => e # all ERRNO errors
84
- log "Exception: #{e.to_s}", level: :error
93
+ rescue ConnectionError => e
94
+ log "Rejected connection from #{remote_ip}:#{remote_port}, #{e.to_s}", level: :warning
95
+ notify_error e
85
96
  rescue StandardError => e
86
- log "Exception: #{e}", exception: e, level: :error
97
+ log "Connection: #{e.to_s}", exception: e, level: :error
98
+ notify_error e, level: :internal
87
99
  ensure
88
100
  close socket, info
89
101
  end
@@ -91,7 +103,7 @@ module RSMP
91
103
  def starting
92
104
  log "Starting supervisor on port #{@supervisor_settings["port"]}",
93
105
  level: :info,
94
- timestamp: RSMP.now_object
106
+ timestamp: @clock.now
95
107
  end
96
108
 
97
109
  def accept? socket, info
@@ -110,30 +122,49 @@ module RSMP
110
122
  end
111
123
  end
112
124
 
125
+ def authorize_ip ip
126
+ return if @supervisor_settings['ips'] == 'all'
127
+ return if @supervisor_settings['ips'].include? ip
128
+ raise ConnectionError.new('guest ip not allowed')
129
+ end
130
+
131
+ def check_max_sites
132
+ max = @supervisor_settings['max_sites']
133
+ if max
134
+ if @proxies.size >= max
135
+ raise ConnectionError.new("maximum of #{max} sites already connected")
136
+ end
137
+ end
138
+ end
139
+
113
140
  def connect socket, info
114
141
  log "Site connected from #{format_ip_and_port(info)}",
115
142
  ip: info[:ip],
116
143
  port: info[:port],
117
144
  level: :info,
118
- timestamp: RSMP.now_object
145
+ timestamp: Clock.now
146
+
147
+ authorize_ip info[:ip]
148
+ check_max_sites
119
149
 
120
150
  proxy = build_proxy({
121
151
  supervisor: self,
152
+ ip: info[:ip],
153
+ port: info[:port],
122
154
  task: @task,
123
- settings: @supervisor_settings[:sites],
155
+ settings: {'collect'=>@supervisor_settings['collect']},
124
156
  socket: socket,
125
157
  info: info,
126
158
  logger: @logger,
127
159
  archive: @archive
128
160
  })
129
161
  @proxies.push proxy
130
-
131
162
  proxy.run # will run until the site disconnects
132
163
  ensure
133
164
  @proxies.delete proxy
134
165
  site_ids_changed
135
166
 
136
- stop if @supervisor_settings['stop_after_first_session']
167
+ stop if @supervisor_settings['one_shot']
137
168
  end
138
169
 
139
170
  def site_ids_changed
@@ -146,9 +177,9 @@ module RSMP
146
177
 
147
178
  def close socket, info
148
179
  if info
149
- log "Connection to #{format_ip_and_port(info)} closed", ip: info[:ip], level: :info, timestamp: RSMP.now_object
180
+ log "Connection to #{format_ip_and_port(info)} closed", ip: info[:ip], level: :info, timestamp: Clock.now
150
181
  else
151
- log "Connection closed", level: :info, timestamp: RSMP.now_object
182
+ log "Connection closed", level: :info, timestamp: Clock.now
152
183
  end
153
184
 
154
185
  socket.close
@@ -170,32 +201,36 @@ module RSMP
170
201
  return site if site
171
202
  wait_for(@site_id_condition,timeout) { find_site site_id }
172
203
  rescue Async::TimeoutError
173
- nil
204
+ raise RSMP::TimeoutError.new "Site '#{site_id}' did not connect within #{timeout}s"
174
205
  end
175
206
 
176
207
  def wait_for_site_disconnect site_id, timeout
177
208
  wait_for(@site_id_condition,timeout) { true unless find_site site_id }
178
209
  rescue Async::TimeoutError
179
- false
210
+ raise RSMP::TimeoutError.new "Site '#{site_id}' did not disconnect within #{timeout}s"
180
211
  end
181
212
 
182
213
  def check_site_id site_id
183
214
  check_site_already_connected site_id
184
- return find_allowed_site_setting site_id
215
+ return site_id_to_site_setting site_id
185
216
  end
186
217
 
187
218
  def check_site_already_connected site_id
188
- raise FatalError.new "Site #{site_id} already connected" if find_site(site_id)
219
+ raise FatalError.new "Site '#{site_id}' already connected" if find_site(site_id)
189
220
  end
190
221
 
191
- def find_allowed_site_setting site_id
222
+ def site_id_to_site_setting site_id
192
223
  return {} unless @supervisor_settings['sites']
193
224
  @supervisor_settings['sites'].each_pair do |id,settings|
194
- if id == :any || id == site_id
225
+ if id == 'guest' || id == site_id
195
226
  return settings
196
227
  end
197
228
  end
198
- raise FatalError.new "site id #{site_id} rejected"
229
+ raise FatalError.new "site id #{site_id} unknown"
230
+ end
231
+
232
+ def ip_to_site_settings ip
233
+ @supervisor_settings['sites'][ip] || @supervisor_settings['sites']['guest']
199
234
  end
200
235
 
201
236
  def aggregated_status_changed site_proxy, component
@@ -14,7 +14,6 @@ module RSMP
14
14
  @ip = options[:ip]
15
15
  @port = options[:port]
16
16
  @status_subscriptions = {}
17
- @status_subscriptions_mutex = Mutex.new
18
17
  @sxl = @site_settings['sxl']
19
18
  @synthetic_id = Supervisor.build_id_from_ip_port @ip, @port
20
19
  end
@@ -33,8 +32,8 @@ module RSMP
33
32
  send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
34
33
  rescue Errno::ECONNREFUSED
35
34
  log "No connection to supervisor at #{@ip}:#{@port}", level: :error
36
- unless @site.site_settings["reconnect_interval"] == :no
37
- log "Will try to reconnect again every #{@site.site_settings["reconnect_interval"]} seconds..", level: :info
35
+ unless @site.site_settings['intervals']['reconnect'] == :no
36
+ log "Will try to reconnect again every #{@site.site_settings['intervals']['reconnect']} seconds..", level: :info
38
37
  @logger.mute @ip, @port
39
38
  end
40
39
  end
@@ -50,12 +49,12 @@ module RSMP
50
49
  @endpoint = Async::IO::Endpoint.tcp(@ip, @port)
51
50
  @socket = @endpoint.connect
52
51
  @stream = Async::IO::Stream.new(@socket)
53
- @protocol = Async::IO::Protocol::Line.new(@stream,RSMP::WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
52
+ @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
54
53
  end
55
54
 
56
55
  def connection_complete
57
56
  super
58
- log "Connection to supervisor established", level: :info
57
+ log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sxl_version}", level: :info
59
58
  start_watchdog
60
59
  end
61
60
 
@@ -66,6 +65,8 @@ module RSMP
66
65
  when StatusUpdate
67
66
  when AggregatedStatus
68
67
  will_not_handle message
68
+ when AggregatedStatusRequest
69
+ process_aggregated_status_request message
69
70
  when CommandRequest
70
71
  process_command_request message
71
72
  when CommandResponse
@@ -109,7 +110,7 @@ module RSMP
109
110
  end
110
111
 
111
112
  def reconnect_delay
112
- interval = @site_settings["reconnect_interval"]
113
+ interval = @site_settings['intervals']['reconnect']
113
114
  log "Waiting #{interval} seconds before trying to reconnect", level: :info
114
115
  @task.sleep interval
115
116
  end
@@ -124,7 +125,7 @@ module RSMP
124
125
 
125
126
  def send_aggregated_status component
126
127
  message = AggregatedStatus.new({
127
- "aSTS" => RSMP.now_string,
128
+ "aSTS" => clock.to_s,
128
129
  "cId" => component.c_id,
129
130
  "fP" => 'NormalControl',
130
131
  "fS" => nil,
@@ -162,6 +163,14 @@ module RSMP
162
163
  sorted
163
164
  end
164
165
 
166
+ def process_aggregated_status_request message
167
+ log "Received #{message.type}", message: message, level: :log
168
+ component_id = message.attributes["cId"]
169
+ component = @site.find_component component_id
170
+ acknowledge message
171
+ send_aggregated_status component
172
+ end
173
+
165
174
  def process_command_request message
166
175
  log "Received #{message.type}", message: message, level: :log
167
176
  component_id = message.attributes["cId"]
@@ -178,7 +187,7 @@ module RSMP
178
187
  end
179
188
  response = CommandResponse.new({
180
189
  "cId"=>component_id,
181
- "cTS"=>RSMP.now_string,
190
+ "cTS"=>clock.to_s,
182
191
  "rvs"=>rvs
183
192
  })
184
193
  acknowledge message
@@ -195,7 +204,7 @@ module RSMP
195
204
  end
196
205
  response = StatusResponse.new({
197
206
  "cId"=>component_id,
198
- "sTs"=>RSMP.now_string,
207
+ "sTs"=>clock.to_s,
199
208
  "sS"=>sS,
200
209
  "mId" => options[:m_id]
201
210
  })
@@ -222,7 +231,7 @@ module RSMP
222
231
  update_list[component] ||= {}
223
232
 
224
233
  subs = @status_subscriptions[component]
225
- now = RSMP::now_object
234
+ now = Time.now # internal timestamp
226
235
 
227
236
  message.attributes["sS"].each do |arg|
228
237
  sCI = arg["sCI"]
@@ -311,12 +320,10 @@ module RSMP
311
320
  end
312
321
  end
313
322
  send_status_updates update_list
314
- rescue StandardError => e
315
- log ["Status update exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
316
323
  end
317
324
 
318
325
  def send_status_updates update_list
319
- now = RSMP.now_string
326
+ now = clock.to_s
320
327
  update_list.each_pair do |component_id,by_code|
321
328
  component = @site.find_component component_id
322
329
  sS = []
@@ -344,7 +351,7 @@ module RSMP
344
351
 
345
352
  def send_alarm
346
353
  message = Alarm.new({
347
- "aSTS"=>RSMP.now_string,
354
+ "aSTS"=>clock.to_s,
348
355
  "fP"=>nil,
349
356
  "fS"=>nil,
350
357
  "se"=>@site.aggregated_status_bools