rsmp 0.8.5 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yaml +21 -0
- data/Gemfile.lock +9 -3
- data/README.md +2 -12
- data/bin/console +1 -1
- data/cucumber.yml +1 -0
- data/documentation/classes_and_modules.md +4 -4
- data/documentation/collecting_message.md +2 -2
- data/documentation/tasks.md +149 -0
- data/lib/rsmp/archive.rb +3 -3
- data/lib/rsmp/cli.rb +32 -4
- data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
- data/lib/rsmp/collect/command_response_collector.rb +1 -1
- data/lib/rsmp/collect/state_collector.rb +1 -1
- data/lib/rsmp/collect/status_collector.rb +2 -1
- data/lib/rsmp/components.rb +3 -3
- data/lib/rsmp/convert/export/json_schema.rb +4 -4
- data/lib/rsmp/convert/import/yaml.rb +1 -1
- data/lib/rsmp/deep_merge.rb +1 -0
- data/lib/rsmp/error.rb +0 -3
- data/lib/rsmp/inspect.rb +1 -1
- data/lib/rsmp/logger.rb +5 -5
- data/lib/rsmp/logging.rb +1 -1
- data/lib/rsmp/message.rb +1 -1
- data/lib/rsmp/node.rb +10 -45
- data/lib/rsmp/proxy.rb +176 -133
- data/lib/rsmp/rsmp.rb +1 -1
- data/lib/rsmp/site.rb +23 -60
- data/lib/rsmp/site_proxy.rb +21 -17
- data/lib/rsmp/supervisor.rb +25 -21
- data/lib/rsmp/supervisor_proxy.rb +58 -29
- data/lib/rsmp/task.rb +84 -0
- data/lib/rsmp/tlc/signal_group.rb +7 -5
- data/lib/rsmp/tlc/signal_plan.rb +2 -2
- data/lib/rsmp/tlc/traffic_controller.rb +146 -53
- data/lib/rsmp/tlc/traffic_controller_site.rb +43 -36
- data/lib/rsmp/version.rb +1 -1
- data/lib/rsmp.rb +1 -1
- metadata +6 -5
- data/lib/rsmp/site_proxy_wait.rb +0 -0
- data/lib/rsmp/wait.rb +0 -16
- data/test.rb +0 -27
data/lib/rsmp/site_proxy.rb
CHANGED
@@ -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
|
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
|
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}")
|
@@ -148,7 +152,7 @@ module RSMP
|
|
148
152
|
end
|
149
153
|
|
150
154
|
def version_acknowledged
|
151
|
-
|
155
|
+
handshake_complete
|
152
156
|
end
|
153
157
|
|
154
158
|
def process_watchdog message
|
@@ -196,7 +200,7 @@ module RSMP
|
|
196
200
|
def subscribe_to_status component_id, status_list, options={}
|
197
201
|
validate_ready 'subscribe to status'
|
198
202
|
m_id = options[:m_id] || RSMP::Message.make_m_id
|
199
|
-
|
203
|
+
|
200
204
|
# additional items can be used when verifying the response,
|
201
205
|
# but must to remove from the subscribe message
|
202
206
|
subscribe_list = status_list.map { |item| item.slice('sCI','n','uRt') }
|
@@ -324,7 +328,7 @@ module RSMP
|
|
324
328
|
log "Using site settings for guest", level: :debug
|
325
329
|
return @settings['guest']
|
326
330
|
end
|
327
|
-
|
331
|
+
|
328
332
|
nil
|
329
333
|
end
|
330
334
|
|
data/lib/rsmp/supervisor.rb
CHANGED
@@ -58,26 +58,31 @@ module RSMP
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
|
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
|
-
|
93
|
+
accept_connection socket, info
|
89
94
|
else
|
90
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
55
|
-
return if @socket
|
71
|
+
def connect_tcp
|
56
72
|
@endpoint = Async::IO::Endpoint.tcp(@ip, @port)
|
57
|
-
|
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
|
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 "
|
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
|
-
|
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 |
|
142
|
-
|
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 =
|
12
|
+
@state = compute_state
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
return 'a' if node.main.
|
17
|
-
return 'c' if node.main.yellow_flash
|
15
|
+
def compute_state
|
16
|
+
return 'a' if node.main.dark?
|
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
|
-
|
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
|
data/lib/rsmp/tlc/signal_plan.rb
CHANGED
@@ -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:
|