rsmp 0.8.4 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yaml +21 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile.lock +51 -67
  5. data/README.md +2 -12
  6. data/bin/console +1 -1
  7. data/cucumber.yml +1 -0
  8. data/documentation/classes_and_modules.md +4 -4
  9. data/documentation/collecting_message.md +2 -2
  10. data/documentation/tasks.md +149 -0
  11. data/lib/rsmp/archive.rb +3 -3
  12. data/lib/rsmp/cli.rb +32 -4
  13. data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
  14. data/lib/rsmp/collect/collector.rb +13 -6
  15. data/lib/rsmp/collect/command_response_collector.rb +1 -1
  16. data/lib/rsmp/collect/state_collector.rb +1 -1
  17. data/lib/rsmp/collect/status_collector.rb +2 -1
  18. data/lib/rsmp/components.rb +3 -3
  19. data/lib/rsmp/convert/export/json_schema.rb +4 -4
  20. data/lib/rsmp/convert/import/yaml.rb +1 -1
  21. data/lib/rsmp/deep_merge.rb +1 -0
  22. data/lib/rsmp/error.rb +0 -3
  23. data/lib/rsmp/inspect.rb +1 -1
  24. data/lib/rsmp/logger.rb +5 -5
  25. data/lib/rsmp/logging.rb +1 -1
  26. data/lib/rsmp/message.rb +1 -1
  27. data/lib/rsmp/node.rb +10 -45
  28. data/lib/rsmp/proxy.rb +184 -134
  29. data/lib/rsmp/rsmp.rb +1 -1
  30. data/lib/rsmp/site.rb +23 -60
  31. data/lib/rsmp/site_proxy.rb +33 -37
  32. data/lib/rsmp/supervisor.rb +25 -21
  33. data/lib/rsmp/supervisor_proxy.rb +58 -29
  34. data/lib/rsmp/task.rb +84 -0
  35. data/lib/rsmp/tlc/signal_group.rb +5 -3
  36. data/lib/rsmp/tlc/signal_plan.rb +2 -2
  37. data/lib/rsmp/tlc/traffic_controller.rb +97 -29
  38. data/lib/rsmp/tlc/traffic_controller_site.rb +43 -36
  39. data/lib/rsmp/version.rb +1 -1
  40. data/lib/rsmp.rb +1 -1
  41. data/rsmp.gemspec +7 -7
  42. metadata +21 -20
  43. data/lib/rsmp/site_proxy_wait.rb +0 -0
  44. data/lib/rsmp/wait.rb +0 -16
  45. data/test.rb +0 -27
@@ -1,6 +1,6 @@
1
1
  # Handles a supervisor connection to a remote client
2
2
 
3
- module RSMP
3
+ module RSMP
4
4
  class SiteProxy < Proxy
5
5
  include Components
6
6
 
@@ -14,33 +14,37 @@ module RSMP
14
14
  @site_id = options[:site_id]
15
15
  end
16
16
 
17
+ # handle communication
18
+ # when we're created, the socket is already open
19
+ def run
20
+ set_state :connected
21
+ start_reader
22
+ wait_for_reader # run until disconnected
23
+ rescue RSMP::ConnectionError => e
24
+ log e, level: :error
25
+ rescue StandardError => e
26
+ notify_error e, level: :internal
27
+ ensure
28
+ close
29
+ end
30
+
17
31
  def revive options
18
32
  super options
19
33
  @supervisor = options[:supervisor]
20
34
  @settings = @supervisor.supervisor_settings.clone
21
35
  end
22
36
 
23
-
24
37
  def inspect
25
38
  "#<#{self.class.name}:#{self.object_id}, #{inspector(
26
39
  :@acknowledgements,:@settings,:@site_settings,:@components
27
40
  )}>"
28
41
  end
42
+
29
43
  def node
30
44
  supervisor
31
45
  end
32
46
 
33
- def start
34
- super
35
- start_reader
36
- end
37
-
38
- def stop
39
- log "Closing connection to site", level: :info
40
- super
41
- end
42
-
43
- def connection_complete
47
+ def handshake_complete
44
48
  super
45
49
  sanitized_sxl_version = RSMP::Schemer.sanitize_version(@site_sxl_version)
46
50
  log "Connection to site #{@site_id} established, using core #{@rsmp_version}, #{@sxl} #{sanitized_sxl_version}", level: :info
@@ -68,7 +72,7 @@ module RSMP
68
72
  else
69
73
  super message
70
74
  end
71
- rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError, TimestampError => e
75
+ rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError => e
72
76
  str = "Rejected #{message.type} message,"
73
77
  dont_acknowledge message, str, "#{e}"
74
78
  notify_error e.exception("#{str}#{e.message} #{message.json}")
@@ -101,13 +105,11 @@ module RSMP
101
105
  "cId" => component,
102
106
  "mId" => m_id
103
107
  })
104
- send_and_optionally_collect message, options do |task|
105
- collector = AggregatedStatusCollector.new(
108
+ send_and_optionally_collect message, options do |collect_options|
109
+ AggregatedStatusCollector.new(
106
110
  self,
107
- options[:collect].merge(task:@task,m_id: m_id, num:1)
111
+ collect_options.merge(task:@task,m_id: m_id, num:1)
108
112
  )
109
- collector.collect
110
- collector
111
113
  end
112
114
  end
113
115
 
@@ -150,7 +152,7 @@ module RSMP
150
152
  end
151
153
 
152
154
  def version_acknowledged
153
- connection_complete
155
+ handshake_complete
154
156
  end
155
157
 
156
158
  def process_watchdog message
@@ -179,14 +181,12 @@ module RSMP
179
181
  "sS" => request_list,
180
182
  "mId" => m_id
181
183
  })
182
- send_and_optionally_collect message, options do |task|
183
- collector = StatusCollector.new(
184
+ send_and_optionally_collect message, options do |collect_options|
185
+ StatusCollector.new(
184
186
  self,
185
187
  status_list,
186
- options[:collect].merge(task:@task,m_id: m_id)
188
+ collect_options.merge(task:@task,m_id: m_id)
187
189
  )
188
- collector.collect
189
- collector
190
190
  end
191
191
  end
192
192
 
@@ -200,7 +200,7 @@ module RSMP
200
200
  def subscribe_to_status component_id, status_list, options={}
201
201
  validate_ready 'subscribe to status'
202
202
  m_id = options[:m_id] || RSMP::Message.make_m_id
203
-
203
+
204
204
  # additional items can be used when verifying the response,
205
205
  # but must to remove from the subscribe message
206
206
  subscribe_list = status_list.map { |item| item.slice('sCI','n','uRt') }
@@ -215,14 +215,12 @@ module RSMP
215
215
  "sS" => subscribe_list,
216
216
  'mId' => m_id
217
217
  })
218
- send_and_optionally_collect message, options do |task|
219
- collector = StatusCollector.new(
218
+ send_and_optionally_collect message, options do |collect_options|
219
+ StatusCollector.new(
220
220
  self,
221
221
  status_list,
222
- options[:collect].merge(task:@task,m_id: m_id)
222
+ collect_options.merge(task:@task,m_id: m_id)
223
223
  )
224
- collector.collect
225
- collector
226
224
  end
227
225
  end
228
226
 
@@ -267,14 +265,12 @@ module RSMP
267
265
  "arg" => command_list,
268
266
  "mId" => m_id
269
267
  })
270
- send_and_optionally_collect message, options do |task|
271
- collector = CommandResponseCollector.new(
268
+ send_and_optionally_collect message, options do |collect_options|
269
+ CommandResponseCollector.new(
272
270
  self,
273
271
  command_list,
274
- options[:collect].merge(task:@task,m_id: m_id)
272
+ collect_options.merge(task:@task,m_id: m_id)
275
273
  )
276
- collector.collect
277
- collector
278
274
  end
279
275
  end
280
276
 
@@ -332,7 +328,7 @@ module RSMP
332
328
  log "Using site settings for guest", level: :debug
333
329
  return @settings['guest']
334
330
  end
335
-
331
+
336
332
  nil
337
333
  end
338
334
 
@@ -58,26 +58,31 @@ module RSMP
58
58
  end
59
59
  end
60
60
 
61
- def start_action
61
+ # listen for connections
62
+ # Async::IO::Endpoint#accept createa an async task that we will wait for
63
+ def run
64
+ log "Starting supervisor on port #{@supervisor_settings["port"]}",
65
+ level: :info,
66
+ timestamp: @clock.now
67
+
62
68
  @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
63
- @endpoint.accept do |socket| # creates async tasks
69
+ tasks = @endpoint.accept do |socket| # creates async tasks
64
70
  handle_connection(socket)
65
71
  rescue StandardError => e
66
72
  notify_error e, level: :internal
67
73
  end
74
+ tasks.each { |task| task.wait }
68
75
  rescue StandardError => e
69
76
  notify_error e, level: :internal
70
77
  end
71
78
 
79
+ # stop
72
80
  def stop
73
81
  log "Stopping supervisor #{@supervisor_settings["site_id"]}", level: :info
74
- @proxies.each { |proxy| proxy.stop }
75
- @proxies.clear
76
82
  super
77
- @tcp_server.close if @tcp_server
78
- @tcp_server = nil
79
83
  end
80
84
 
85
+ # handle an incoming connction by either accepting of rejecting it
81
86
  def handle_connection socket
82
87
  remote_port = socket.remote_address.ip_port
83
88
  remote_hostname = socket.remote_address.ip_address
@@ -85,9 +90,9 @@ module RSMP
85
90
 
86
91
  info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:Clock.now}
87
92
  if accept? socket, info
88
- connect socket, info
93
+ accept_connection socket, info
89
94
  else
90
- reject socket, info
95
+ reject_connection socket, info
91
96
  end
92
97
  rescue ConnectionError => e
93
98
  log "Rejected connection from #{remote_ip}:#{remote_port}, #{e.to_s}", level: :warning
@@ -99,12 +104,6 @@ module RSMP
99
104
  close socket, info
100
105
  end
101
106
 
102
- def starting
103
- log "Starting supervisor on port #{@supervisor_settings["port"]}",
104
- level: :info,
105
- timestamp: @clock.now
106
- end
107
-
108
107
  def accept? socket, info
109
108
  true
110
109
  end
@@ -143,7 +142,8 @@ module RSMP
143
142
  message.attribute('siteId').first['sId']
144
143
  end
145
144
 
146
- def connect socket, info
145
+ # accept an incoming connecting by creating and starting a proxy
146
+ def accept_connection socket, info
147
147
  log "Site connected from #{format_ip_and_port(info)}",
148
148
  ip: info[:ip],
149
149
  port: info[:port],
@@ -182,7 +182,8 @@ module RSMP
182
182
  proxy = build_proxy settings.merge(site_id:id) # keep the id learned by peeking above
183
183
  @proxies.push proxy
184
184
  end
185
- proxy.run # will run until the site disconnects
185
+ proxy.start # will run until the site disconnects
186
+ proxy.wait
186
187
  ensure
187
188
  site_ids_changed
188
189
  stop if @supervisor_settings['one_shot']
@@ -192,7 +193,7 @@ module RSMP
192
193
  @site_id_condition.signal
193
194
  end
194
195
 
195
- def reject socket, info
196
+ def reject_connection socket, info
196
197
  log "Site rejected", ip: info[:ip], level: :info
197
198
  end
198
199
 
@@ -224,10 +225,13 @@ module RSMP
224
225
  nil
225
226
  end
226
227
 
227
- def wait_for_site site_id, timeout
228
+ def wait_for_site site_id, timeout:
228
229
  site = find_site site_id
229
230
  return site if site
230
- wait_for(@site_id_condition,timeout) { find_site site_id }
231
+ wait_for_condition(@site_id_condition,timeout:timeout) do
232
+ find_site site_id
233
+ end
234
+
231
235
  rescue Async::TimeoutError
232
236
  if site_id == :any
233
237
  str = "No site connected"
@@ -237,8 +241,8 @@ module RSMP
237
241
  raise RSMP::TimeoutError.new "#{str} within #{timeout}s"
238
242
  end
239
243
 
240
- def wait_for_site_disconnect site_id, timeout
241
- wait_for(@site_id_condition,timeout) { true unless find_site site_id }
244
+ def wait_for_site_disconnect site_id, timeout:
245
+ wait_for_condition(@site_id_condition,timeout:timeout) { true unless find_site site_id }
242
246
  rescue Async::TimeoutError
243
247
  raise RSMP::TimeoutError.new "Site '#{site_id}' did not disconnect within #{timeout}s"
244
248
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'digest'
4
4
 
5
- module RSMP
5
+ module RSMP
6
6
  class SupervisorProxy < Proxy
7
7
 
8
8
  attr_reader :supervisor_id, :site
@@ -22,44 +22,70 @@ module RSMP
22
22
  site
23
23
  end
24
24
 
25
- def start
25
+ # handle communication
26
+ # if disconnected, then try to reconnect
27
+ def run
28
+ loop do
29
+ connect
30
+ start_reader
31
+ start_handshake
32
+ wait_for_reader # run until disconnected
33
+ break if reconnect_delay == false
34
+ rescue Restart
35
+ @logger.mute @ip, @port
36
+ raise
37
+ rescue RSMP::ConnectionError => e
38
+ log e, level: :error
39
+ break if reconnect_delay == false
40
+ rescue StandardError => e
41
+ notify_error e, level: :internal
42
+ break if reconnect_delay == false
43
+ ensure
44
+ close
45
+ stop_subtasks
46
+ end
47
+ end
48
+
49
+ def start_handshake
50
+ send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
51
+ end
52
+
53
+ # connect to the supervisor and initiate handshake supervisor
54
+ def connect
26
55
  log "Connecting to supervisor at #{@ip}:#{@port}", level: :info
27
- super
28
- connect
56
+ set_state :connecting
57
+ connect_tcp
29
58
  @logger.unmute @ip, @port
30
59
  log "Connected to supervisor at #{@ip}:#{@port}", level: :info
31
- start_reader
32
- send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
33
60
  rescue SystemCallError => e
34
- log "Could not connect to supervisor at #{@ip}:#{@port}: Errno #{e.errno} #{e}", level: :error
35
- retry_notice
61
+ raise ConnectionError.new "Could not connect to supervisor at #{@ip}:#{@port}: Errno #{e.errno} #{e}"
36
62
  rescue StandardError => e
37
- log "Error while connecting to supervisor at #{@ip}:#{@port}: #{e}", level: :error
38
- retry_notice
39
- end
40
-
41
- def retry_notice
42
- unless @site.site_settings['intervals']['reconnect'] == :no
43
- log "Will try to reconnect again every #{@site.site_settings['intervals']['reconnect']} seconds..", level: :info
44
- @logger.mute @ip, @port
45
- end
63
+ raise ConnectionError.new "Error while connecting to supervisor at #{@ip}:#{@port}: #{e}"
46
64
  end
47
65
 
48
- def stop
49
- log "Closing connection to supervisor", level: :info
66
+ def stop_task
50
67
  super
51
68
  @last_status_sent = nil
52
69
  end
53
70
 
54
- def connect
55
- return if @socket
71
+ def connect_tcp
56
72
  @endpoint = Async::IO::Endpoint.tcp(@ip, @port)
57
- @socket = @endpoint.connect
73
+
74
+ # Async::IO::Endpoint#connect renames the current task. run in a subtask to avoid this see issue #22
75
+ @task.async do |task|
76
+ task.annotate 'socket task'
77
+ # this timeout is a workaround for #connect hanging on windows if the other side is not present yet
78
+ task.with_timeout 1.1 do
79
+ @socket = @endpoint.connect
80
+ end
81
+ end.wait
82
+
58
83
  @stream = Async::IO::Stream.new(@socket)
59
84
  @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
85
+ set_state :connected
60
86
  end
61
87
 
62
- def connection_complete
88
+ def handshake_complete
63
89
  super
64
90
  sanitized_sxl_version = RSMP::Schemer.sanitize_version(sxl_version)
65
91
  log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sanitized_sxl_version}", level: :info
@@ -114,16 +140,19 @@ module RSMP
114
140
  end
115
141
 
116
142
  def reconnect_delay
143
+ return false if @site_settings['intervals']['reconnect'] == :no
117
144
  interval = @site_settings['intervals']['reconnect']
118
- log "Waiting #{interval} seconds before trying to reconnect", level: :info
145
+ log "Will try to reconnect again every #{interval} seconds...", level: :info
146
+ @logger.mute @ip, @port
119
147
  @task.sleep interval
148
+ true
120
149
  end
121
150
 
122
151
  def version_accepted message
123
152
  log "Received Version message, using RSMP #{@rsmp_version}", message: message, level: :log
124
153
  start_timer
125
154
  acknowledge message
126
- connection_complete
155
+ handshake_complete
127
156
  @version_determined = true
128
157
  end
129
158
 
@@ -138,8 +167,8 @@ module RSMP
138
167
  "mId" => m_id,
139
168
  })
140
169
 
141
- send_and_optionally_collect message, options do |task|
142
- wait_for_acknowledgement task, options[:collect], m_id
170
+ send_and_optionally_collect message, options do |collect_options|
171
+ Collector.new self, collect_options.merge(task:@task, type: 'MessageAck')
143
172
  end
144
173
  end
145
174
 
@@ -234,7 +263,7 @@ module RSMP
234
263
  update_list = {}
235
264
  component = message.attributes["cId"]
236
265
  @status_subscriptions[component] ||= {}
237
- update_list[component] ||= {}
266
+ update_list[component] ||= {}
238
267
  now = Time.now # internal timestamp
239
268
  subs = @status_subscriptions[component]
240
269
 
@@ -299,7 +328,7 @@ module RSMP
299
328
  by_name.each_pair do |name,subscription|
300
329
  current = nil
301
330
  should_send = false
302
- if subscription[:interval] == 0
331
+ if subscription[:interval] == 0
303
332
  # send as soon as the data changes
304
333
  if component_object
305
334
  current, age = *(component_object.get_status code, name)
data/lib/rsmp/task.rb ADDED
@@ -0,0 +1,84 @@
1
+ module RSMP
2
+ class Restart < StandardError
3
+ end
4
+
5
+ module Task
6
+ attr_reader :task
7
+
8
+ def initialize_task
9
+ @task = nil
10
+ end
11
+
12
+ # start our async tasks and return immediately
13
+ # run() will be called inside the task to perform actual long-running work
14
+ def start
15
+ return if @task
16
+ Async do |task|
17
+ task.annotate "#{self.class.name} main task"
18
+ @task = task
19
+ run
20
+ stop_subtasks
21
+ @task = nil
22
+ end
23
+ self
24
+ end
25
+
26
+ # initiate restart by raising a Restart exception
27
+ def restart
28
+ raise Restart.new "restart initiated by #{self.class.name}:#{object_id}"
29
+ end
30
+
31
+ # get the status of our task, or nil of no task
32
+ def task_status
33
+ @task.status if @task
34
+ end
35
+
36
+ # perform any long-running work
37
+ # the method will be called from an async task, and should not return
38
+ # if subtasks are needed, the method should call wait() on each of them
39
+ # once running, ready() must be called
40
+ def run
41
+ start_subtasks
42
+ end
43
+
44
+ # wait for our task to complete
45
+ def wait
46
+ @task.wait if @task
47
+ end
48
+
49
+ # stop our task
50
+ def stop
51
+ stop_subtasks
52
+ stop_task if @task
53
+ end
54
+
55
+ def stop_subtasks
56
+ end
57
+
58
+ # stop our task and any subtask
59
+ def stop_task
60
+ @task.stop
61
+ @task = nil
62
+ end
63
+
64
+ # wait for an async condition to signal, then yield to block
65
+ # if block returns true we're done. otherwise, wait again
66
+ def wait_for_condition condition, timeout:, task:Async::Task.current, &block
67
+ unless task
68
+ raise RuntimeError.new("Can't wait without a task")
69
+ end
70
+ task.with_timeout(timeout) do
71
+ while task.running?
72
+ value = condition.wait
73
+ return value unless block
74
+ result = yield value
75
+ return result if result
76
+ end
77
+ raise RuntimeError.new("Can't wait for condition because task #{task.object_id} #{task.annotation} is not running")
78
+ end
79
+ rescue Async::TimeoutError
80
+ raise RSMP::TimeoutError.new
81
+ end
82
+
83
+ end
84
+ end
@@ -9,25 +9,27 @@ module RSMP
9
9
  end
10
10
 
11
11
  def timer
12
- @state = get_state
12
+ @state = compute_state
13
13
  end
14
14
 
15
- def get_state
15
+ def compute_state
16
16
  return 'a' if node.main.dark_mode
17
17
  return 'c' if node.main.yellow_flash
18
18
 
19
19
  cycle_counter = node.main.cycle_counter
20
20
 
21
21
  if node.main.startup_sequence_active
22
- @state = node.main.startup_state || 'a'
22
+ return node.main.startup_state || 'a'
23
23
  end
24
24
 
25
25
  default = 'a' # phase a means disabled/dark
26
26
  plan = node.main.current_plan
27
27
  return default unless plan
28
28
  return default unless plan.states
29
+
29
30
  states = plan.states[c_id]
30
31
  return default unless states
32
+
31
33
  state = states[cycle_counter]
32
34
  return default unless state =~ /[a-hA-G0-9N-P]/ # valid signal group states
33
35
  state
@@ -1,8 +1,8 @@
1
1
  module RSMP
2
2
  module TLC
3
3
  # A Traffic Light Controller Signal Plan.
4
- # A signal plan is a description of how all signal groups should change
5
- # state over time.
4
+ # A signal plan is a description of how all signal groups should change
5
+ # state over time.
6
6
  class SignalPlan
7
7
  attr_reader :nr, :states, :dynamic_bands
8
8
  def initialize nr:, states:, dynamic_bands: