rsmp 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/Gemfile.lock +1 -1
- data/documentation/configuration.md +53 -2
- data/lib/rsmp/component/component_base.rb +10 -1
- data/lib/rsmp/component/component_proxy.rb +1 -1
- data/lib/rsmp/component/components.rb +24 -2
- data/lib/rsmp/node/site/site.rb +21 -7
- data/lib/rsmp/node/site/status.rb +39 -0
- data/lib/rsmp/node/supervisor/modules/connection.rb +20 -4
- data/lib/rsmp/node/supervisor/modules/sites.rb +2 -0
- data/lib/rsmp/options/schemas/required_settings.json +20 -12
- data/lib/rsmp/options/schemas/supervisor_site.json +1 -1
- data/lib/rsmp/options/site_options.rb +1 -0
- data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
- data/lib/rsmp/proxy/modules/receive.rb +32 -2
- data/lib/rsmp/proxy/modules/send.rb +23 -3
- data/lib/rsmp/proxy/modules/version_validation.rb +76 -0
- data/lib/rsmp/proxy/modules/versions.rb +21 -22
- data/lib/rsmp/proxy/proxy.rb +21 -3
- data/lib/rsmp/proxy/site/modules/aggregated_status.rb +22 -2
- data/lib/rsmp/proxy/site/modules/commands.rb +1 -1
- data/lib/rsmp/proxy/site/site_proxy.rb +22 -13
- data/lib/rsmp/proxy/site/sxl_selection.rb +13 -22
- data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +56 -1
- data/lib/rsmp/proxy/supervisor/modules/alarms.rb +11 -4
- data/lib/rsmp/proxy/supervisor/modules/commands.rb +4 -3
- data/lib/rsmp/proxy/supervisor/modules/message_buffer.rb +6 -1
- data/lib/rsmp/proxy/supervisor/modules/status.rb +46 -1
- data/lib/rsmp/proxy/supervisor/modules/status_updates.rb +17 -1
- data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +12 -16
- data/lib/rsmp/schema/validation.rb +2 -1
- data/lib/rsmp/version.rb +1 -1
- data/schemas/core/3.1.2/definitions.json +5 -0
- data/schemas/core/3.1.2/status_subscribe.json +1 -1
- data/schemas/core/3.1.3/definitions.json +5 -0
- data/schemas/core/3.1.3/status_subscribe.json +1 -1
- data/schemas/core/3.1.4/definitions.json +5 -0
- data/schemas/core/3.1.4/status_subscribe.json +1 -1
- data/schemas/core/3.1.5/definitions.json +5 -0
- data/schemas/core/3.1.5/status_subscribe.json +1 -1
- data/schemas/core/3.2.0/definitions.json +5 -0
- data/schemas/core/3.2.0/status_subscribe.json +1 -1
- data/schemas/core/3.2.1/definitions.json +5 -0
- data/schemas/core/3.2.1/status_subscribe.json +1 -1
- data/schemas/core/3.2.2/definitions.json +5 -0
- data/schemas/core/3.2.2/status_subscribe.json +1 -1
- data/schemas/core/3.3.0/aggregated_status.json +17 -11
- data/schemas/core/3.3.0/aggregated_status_request.json +4 -4
- data/schemas/core/3.3.0/component_list.json +1 -1
- data/schemas/core/3.3.0/definitions.json +23 -2
- data/schemas/core/3.3.0/status_subscribe.json +14 -4
- data/schemas/core/3.3.0/version.json +46 -26
- data/schemas/tlc/1.0.10/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.13/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.14/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.15/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.7/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.8/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.9/defs/definitions.json +5 -0
- data/schemas/tlc/1.1.0/defs/definitions.json +5 -0
- data/schemas/tlc/1.2.0/defs/definitions.json +5 -0
- data/schemas/tlc/1.2.1/defs/definitions.json +5 -0
- data/schemas/tlc/1.3.0/defs/definitions.json +7 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66bd0079bb76c72ad04ce15f191ac3f33a06bcaf4f941c8d23f10b8239c3f308
|
|
4
|
+
data.tar.gz: 34f6fb37b13637ffe1bdf3f92064e103fc3b8b798fce11e2764d0ab810f23190
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 140f8cd7c0f9f5b542d4b4f47fd4d99095ff7eecaaf784bc6b6ae92e5dee1014b4f0a6338159c21e3bdda86e3ef06764f17b9d0055c3880b213a2c18e0e18708
|
|
7
|
+
data.tar.gz: ab5da12c73a27485b2e94d73cc2090adce96b69aa97dbb4530b5d5819aad20ecf0fa466d2b3b3f5068e678cbe41fcfefdf9808155c25656ad4fb52e78432ce7c
|
data/CHANGELOG.md
CHANGED
|
@@ -735,3 +735,6 @@ Initial release.
|
|
|
735
735
|
- add `start` and `stop` aliases to queues for use with proxy collection factories
|
|
736
736
|
- fix status collectors to honor `component:` and `responses:`; the misspelled `reponses:` option is no longer used
|
|
737
737
|
|
|
738
|
+
## 0.53.0
|
|
739
|
+
- update core 3.3.0 support to sync with latest core 3.3.0 draft
|
|
740
|
+
- allow fractional status subscription intervals for all older core versions to align with their specs.
|
data/Gemfile.lock
CHANGED
|
@@ -21,8 +21,11 @@ and `required_supervisor_settings.json` schemas then check required settings aft
|
|
|
21
21
|
supervisor site inheritance are resolved:
|
|
22
22
|
|
|
23
23
|
- Every configured signal plan must have a `cycle_time`.
|
|
24
|
-
-
|
|
25
|
-
|
|
24
|
+
- Before Core 3.3.0, exactly one `main` component is required. Sites supply a default main component
|
|
25
|
+
when omitted; supervisor site settings can inherit it from `default`. From Core 3.3.0, a main
|
|
26
|
+
component is not used. With `core_version: "3.3.0"`, no default components are added, even if
|
|
27
|
+
`components` is omitted. Configurations that allow older Core versions (including an omitted
|
|
28
|
+
`core_version` or `"all"`) retain the default main component.
|
|
26
29
|
- A site in `server` role must have a `port`, either explicitly or from its first supervisor endpoint.
|
|
27
30
|
With `supervisors: []`, the port must be explicit.
|
|
28
31
|
|
|
@@ -36,6 +39,54 @@ Earlier Core versions retain their historical wire formats for compatibility. In
|
|
|
36
39
|
|
|
37
40
|
Legacy SXL strings such as `1.1` and `1.2` remain supported for connections using a Core version before 3.3.0. Their three-part equivalents are used only for internal schema lookup.
|
|
38
41
|
|
|
42
|
+
## Core 3.3.0
|
|
43
|
+
|
|
44
|
+
Preliminary Core 3.3.0 support follows `rsmp_core` branch `3.3.0` at commit
|
|
45
|
+
`1aa02faf82da4f47fee57c43c546c092420aa576`, including PR #318. These rules apply only
|
|
46
|
+
when Core 3.3.0 is selected. Earlier Core versions retain their existing behavior.
|
|
47
|
+
|
|
48
|
+
SXL responses use `ok`, `unsupported`, `mismatch` and `expected`. Only `ok` enables
|
|
49
|
+
an SXL, at the exact offered version. `supported` is diagnostic and does not trigger
|
|
50
|
+
fallback. A connection can become ready even if no SXL is accepted.
|
|
51
|
+
|
|
52
|
+
Set `use_alarms: false` in the supervisor's `default` or per-site configuration to
|
|
53
|
+
disable every Alarm message in both directions on a 3.3.0 connection. The default
|
|
54
|
+
is true.
|
|
55
|
+
|
|
56
|
+
The 3.3.0 handshake ends with ComponentList followed by a site-wide AggregatedStatus.
|
|
57
|
+
Readiness follows acknowledgement of that status; either MessageAck or MessageNotAck
|
|
58
|
+
completes this step. The initial status is mandatory even with `send_after_connect: false`.
|
|
59
|
+
|
|
60
|
+
AggregatedStatus has no `cId`, `fP` or `fS`. All false bits mean normal operation;
|
|
61
|
+
bits 2, 6 and 8 are always false in site messages. Site-wide conditions include all
|
|
62
|
+
components and active alarm priorities, including SXLs rejected on a connection.
|
|
63
|
+
Read received bits through `SiteProxy#site_aggregated_status` and override
|
|
64
|
+
`Supervisor#site_aggregated_status_changed(site_proxy)` for notifications.
|
|
65
|
+
For 3.3.0, call `request_aggregated_status` or `request_aggregated_status_and_collect`
|
|
66
|
+
without a component. A supplied component argument is ignored on the wire. Legacy
|
|
67
|
+
requests remain component-addressed.
|
|
68
|
+
|
|
69
|
+
Use `add_component`, `remove_component` and `ComponentBase#update_metadata` to update
|
|
70
|
+
the inventory. Ready 3.3.0 connections receive a complete ComponentList after a change;
|
|
71
|
+
disconnected connections receive the current list in their next handshake. The
|
|
72
|
+
supervisor preserves received ordering and retains unchanged component objects.
|
|
73
|
+
Component names are optional on the wire but must be nonempty strings when supplied.
|
|
74
|
+
The supervisor uses the component ID when a list entry omits its name, including
|
|
75
|
+
when a later list removes a previously supplied name.
|
|
76
|
+
Removing the last component is prohibited because the Core 3.3.0 schema requires a
|
|
77
|
+
nonempty ComponentList.
|
|
78
|
+
|
|
79
|
+
Subscription intervals use `uRt`, an integer or decimal
|
|
80
|
+
number represented as a string, e.g. `"2"` or `"0.5"`. Negative intervals are invalid.
|
|
81
|
+
Zero disables periodic updates and is valid only with `sOc: true`.
|
|
82
|
+
The Core source and vendored 3.3.0 schemas include this correction to the pinned baseline.
|
|
83
|
+
The isolated main-component requirement in `site_configuration.rst` is treated as
|
|
84
|
+
obsolete for 3.3.0 in favor of the component model and site-wide status definitions.
|
|
85
|
+
|
|
86
|
+
Validator follow-up should cover the new Version statuses and failures, both alarm
|
|
87
|
+
directions, site-wide status bits and fields, final-handshake ACK/NotAck, component
|
|
88
|
+
changes, and accepted-SXL filtering during initial synchronization and buffer replay.
|
|
89
|
+
|
|
39
90
|
## Loading a configuration file
|
|
40
91
|
|
|
41
92
|
Use the option classes to load a YAML configuration file:
|
|
@@ -41,8 +41,10 @@ module RSMP
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def update_metadata(type: nil, name: nil)
|
|
44
|
+
changed = (type && type != @component_type) || (name && name != @name)
|
|
44
45
|
@component_type = type if type
|
|
45
46
|
@name = name if name
|
|
47
|
+
@node.component_list_changed if changed
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def now
|
|
@@ -73,7 +75,14 @@ module RSMP
|
|
|
73
75
|
raise ArgumentError, 'aggregated status must be an Array' unless status.is_a? Array
|
|
74
76
|
|
|
75
77
|
input = status & AGGREGATED_STATUS_KEYS
|
|
76
|
-
|
|
78
|
+
if @node.respond_to?(:site_wide_aggregated_status?) && @node.site_wide_aggregated_status?
|
|
79
|
+
bits = AGGREGATED_STATUS_KEYS.map { |key| input.include?(key) }
|
|
80
|
+
return if bits == @aggregated_status_bools
|
|
81
|
+
|
|
82
|
+
@aggregated_status = input
|
|
83
|
+
else
|
|
84
|
+
return unless input != @aggregated_status
|
|
85
|
+
end
|
|
77
86
|
|
|
78
87
|
AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
|
|
79
88
|
@aggregated_status_bools[index] = status.include?(key)
|
|
@@ -39,7 +39,7 @@ module RSMP
|
|
|
39
39
|
|
|
40
40
|
def update_rate_set?(subscription_list, status_code, status_name)
|
|
41
41
|
urt = subscription_list.dig(c_id, status_code, status_name, 'uRt')
|
|
42
|
-
urt.
|
|
42
|
+
urt.to_f.positive?
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def should_check_repeats?(subscription_list, status_code, status_name)
|
|
@@ -13,7 +13,7 @@ module RSMP
|
|
|
13
13
|
def setup_components(settings)
|
|
14
14
|
return unless settings
|
|
15
15
|
|
|
16
|
-
check_main_component settings
|
|
16
|
+
check_main_component settings unless core_3_3_components?
|
|
17
17
|
settings.each_pair do |type, components_by_type|
|
|
18
18
|
next unless components_by_type
|
|
19
19
|
|
|
@@ -30,6 +30,10 @@ module RSMP
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def core_3_3_components?
|
|
34
|
+
@site_settings && @site_settings['core_version'] == '3.3.0'
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def check_main_component(settings)
|
|
34
38
|
raise ConfigurationError, 'main component must be defined' unless settings['main'] && settings['main'].size >= 1
|
|
35
39
|
return unless settings['main'].size > 1
|
|
@@ -39,11 +43,29 @@ module RSMP
|
|
|
39
43
|
|
|
40
44
|
def add_component(component)
|
|
41
45
|
@components[component.c_id] = component
|
|
46
|
+
component_list_changed
|
|
47
|
+
component
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def component_list_changed; end
|
|
51
|
+
|
|
52
|
+
def remove_component(id)
|
|
53
|
+
raise ConfigurationError, 'Cannot remove the legacy main component' if @main&.c_id == id &&
|
|
54
|
+
@site_settings['core_version'] != '3.3.0'
|
|
55
|
+
|
|
56
|
+
if core_3_3_components? && @components.size == 1 && @components.key?(id)
|
|
57
|
+
raise ConfigurationError, 'Core 3.3 ComponentList must not be empty'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
removed = @components.delete(id)
|
|
61
|
+
@main = nil if removed == @main
|
|
62
|
+
component_list_changed if removed
|
|
63
|
+
removed
|
|
42
64
|
end
|
|
43
65
|
|
|
44
66
|
def natural_sort_key(value)
|
|
45
67
|
value.to_s.split(/(\d+)/).map do |part|
|
|
46
|
-
part.match?(/\A\d+\z/) ? part.to_i : part
|
|
68
|
+
part.match?(/\A\d+\z/) ? [0, part.to_i, part.length] : [1, part]
|
|
47
69
|
end
|
|
48
70
|
end
|
|
49
71
|
|
data/lib/rsmp/node/site/site.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
require_relative 'status'
|
|
2
|
+
|
|
1
3
|
module RSMP
|
|
2
4
|
# RSMP site implementation that manages proxies and components.
|
|
3
5
|
class Site < Node
|
|
4
6
|
include Components
|
|
7
|
+
include SiteStatus
|
|
5
8
|
include SiteConnections
|
|
6
9
|
|
|
7
10
|
attr_reader :core_version, :site_settings, :logger, :proxies, :ready_condition
|
|
@@ -127,12 +130,6 @@ module RSMP
|
|
|
127
130
|
start_status_timer(parent: parent)
|
|
128
131
|
end
|
|
129
132
|
|
|
130
|
-
def aggregated_status_changed(component, _options = {})
|
|
131
|
-
@proxies.each do |proxy|
|
|
132
|
-
proxy.send_aggregated_status component
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
133
|
def alarm_acknowledged(alarm_state)
|
|
137
134
|
send_alarm AlarmAcknowledged.new(alarm_state.to_hash)
|
|
138
135
|
end
|
|
@@ -143,11 +140,28 @@ module RSMP
|
|
|
143
140
|
|
|
144
141
|
def alarm_activated_or_deactivated(alarm_state)
|
|
145
142
|
send_alarm AlarmIssue.new(alarm_state.to_hash)
|
|
143
|
+
notify_site_status_change
|
|
146
144
|
end
|
|
147
145
|
|
|
148
146
|
def send_alarm(alarm)
|
|
147
|
+
# Snapshot before any connection encodes the message for its SXL version.
|
|
148
|
+
independent = @proxies.any? { |proxy| proxy.respond_to?(:core_3_3_0_or_newer?) && proxy.core_3_3_0_or_newer? }
|
|
149
|
+
attributes = JSON.parse(JSON.generate(alarm.attributes)) if independent
|
|
149
150
|
@proxies.each do |proxy|
|
|
150
|
-
|
|
151
|
+
next unless proxy.use_alarms?
|
|
152
|
+
|
|
153
|
+
send_connection_alarm(proxy, alarm, attributes)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def send_connection_alarm(proxy, alarm, attributes)
|
|
158
|
+
return if proxy.core_3_3_0_or_newer? && !proxy.sxl_message_allowed?(alarm)
|
|
159
|
+
|
|
160
|
+
alarm = alarm.class.new(JSON.parse(JSON.generate(attributes)).merge('mId' => Message.make_m_id)) if attributes
|
|
161
|
+
if proxy.core_3_3_0_or_newer? && !proxy.ready?
|
|
162
|
+
proxy.buffer_message alarm
|
|
163
|
+
else
|
|
164
|
+
proxy.send_generated_message alarm
|
|
151
165
|
end
|
|
152
166
|
end
|
|
153
167
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
# Site-wide status and component change notifications for Core 3.3.
|
|
3
|
+
module SiteStatus
|
|
4
|
+
def aggregated_status_changed(component, _options = {})
|
|
5
|
+
@proxies.each do |proxy|
|
|
6
|
+
if proxy.core_3_3_0_or_newer?
|
|
7
|
+
proxy.send_changed_site_status
|
|
8
|
+
else
|
|
9
|
+
proxy.send_aggregated_status component
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def site_wide_aggregated_status?
|
|
15
|
+
@site_settings['core_version'] == '3.3.0' || @proxies&.any?(&:core_3_3_0_or_newer?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Core 3.3 status includes every SXL, independently of connection negotiation.
|
|
19
|
+
def site_aggregated_status
|
|
20
|
+
bits = Array.new(8, false)
|
|
21
|
+
@components.each_value do |component|
|
|
22
|
+
[0, 2, 3, 4, 6].each { |index| bits[index] ||= component.aggregated_status_bools[index] }
|
|
23
|
+
component.alarms.each_value do |alarm|
|
|
24
|
+
bits[alarm.priority + 1] = true if alarm.active && (1..3).cover?(alarm.priority)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
bits
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def notify_site_status_change
|
|
31
|
+
@proxies.each { |proxy| proxy.send_changed_site_status if proxy.core_3_3_0_or_newer? }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def component_list_changed
|
|
35
|
+
@proxies&.each { |proxy| proxy.send_component_list if proxy.core_3_3_0_or_newer? && proxy.ready? }
|
|
36
|
+
notify_site_status_change if @proxies
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -92,11 +92,28 @@ module RSMP
|
|
|
92
92
|
site_id: site_id,
|
|
93
93
|
site_settings: site_settings
|
|
94
94
|
}
|
|
95
|
+
rescue HandshakeError => e
|
|
96
|
+
reject_initial_version(protocol, e)
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def reject_initial_version(protocol, error)
|
|
101
|
+
attributes = Message.parse_attributes(protocol.peek_line)
|
|
102
|
+
id = attributes['mId']
|
|
103
|
+
if id.is_a?(String) && /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i.match?(id)
|
|
104
|
+
protocol.write_lines(JSON.generate('mType' => 'rSMsg', 'type' => 'MessageNotAck',
|
|
105
|
+
'oMId' => id, 'rea' => error.message))
|
|
106
|
+
end
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def retrieve_site_id(protocol)
|
|
98
110
|
version_message = peek_version_message protocol
|
|
99
|
-
version_message.
|
|
111
|
+
ids = version_message.attributes['siteId']
|
|
112
|
+
unless ids.is_a?(Array) && ids.first.is_a?(Hash) && ids.first['sId'].is_a?(String)
|
|
113
|
+
raise HandshakeError, 'Invalid siteId in Version request'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
ids.first['sId']
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
def setup_proxy(proxy, settings, id)
|
|
@@ -112,10 +129,9 @@ module RSMP
|
|
|
112
129
|
proxy
|
|
113
130
|
end
|
|
114
131
|
|
|
115
|
-
def validate_and_start_proxy(proxy,
|
|
132
|
+
def validate_and_start_proxy(proxy, _protocol)
|
|
116
133
|
proxy.setup_site_settings
|
|
117
|
-
|
|
118
|
-
log "Validating using core version #{proxy.core_version}", level: :debug
|
|
134
|
+
|
|
119
135
|
proxy.start
|
|
120
136
|
proxy.wait
|
|
121
137
|
ensure
|
|
@@ -3,17 +3,7 @@
|
|
|
3
3
|
"$comment": "Validate after applying defaults and inherited site settings; structural validation uses the other config schemas.",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
6
|
-
"components": {
|
|
7
|
-
"type": "object",
|
|
8
|
-
"required": ["main"],
|
|
9
|
-
"properties": {
|
|
10
|
-
"main": {
|
|
11
|
-
"type": "object",
|
|
12
|
-
"minProperties": 1,
|
|
13
|
-
"maxProperties": 1
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
},
|
|
6
|
+
"components": { "type": "object" },
|
|
17
7
|
"signal_plans": {
|
|
18
8
|
"type": "object",
|
|
19
9
|
"additionalProperties": {
|
|
@@ -26,5 +16,23 @@
|
|
|
26
16
|
"required": ["connection_role"],
|
|
27
17
|
"properties": { "connection_role": { "const": "server" } }
|
|
28
18
|
},
|
|
29
|
-
"then": { "required": ["port"] }
|
|
19
|
+
"then": { "required": ["port"] },
|
|
20
|
+
"allOf": [
|
|
21
|
+
{
|
|
22
|
+
"if": {
|
|
23
|
+
"required": ["core_version"],
|
|
24
|
+
"properties": { "core_version": { "const": "3.3.0" } }
|
|
25
|
+
},
|
|
26
|
+
"else": {
|
|
27
|
+
"properties": {
|
|
28
|
+
"components": {
|
|
29
|
+
"required": ["main"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"main": { "type": "object", "minProperties": 1, "maxProperties": 1 }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
30
38
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"intervals": { "$ref": "config_common.json#/$defs/intervals" },
|
|
25
25
|
"timeouts": { "$ref": "config_common.json#/$defs/timeouts" },
|
|
26
26
|
"send_after_connect": { "type": "boolean" },
|
|
27
|
-
"
|
|
27
|
+
"use_alarms": { "type": "boolean" },
|
|
28
28
|
"message_buffer": { "$ref": "config_common.json#/$defs/message_buffer" },
|
|
29
29
|
"skip_validation": { "type": "array", "items": { "type": "string" } },
|
|
30
30
|
"security_codes": { "$ref": "config_common.json#/$defs/security_codes" },
|
|
@@ -56,6 +56,7 @@ module RSMP
|
|
|
56
56
|
|
|
57
57
|
def apply_defaults(options)
|
|
58
58
|
defaults = defaults()
|
|
59
|
+
defaults['components'] = {} if options['core_version'] == '3.3.0'
|
|
59
60
|
defaults['components']['main'] = options['components']['main'] if options.dig('components', 'main')
|
|
60
61
|
data = defaults.deep_merge(options)
|
|
61
62
|
port = data.dig('supervisors', 0, 'port')
|
|
@@ -111,6 +111,7 @@ module RSMP
|
|
|
111
111
|
|
|
112
112
|
@acknowledgements[original.m_id] = message
|
|
113
113
|
@acknowledgement_condition.signal message
|
|
114
|
+
initial_status_acknowledged(original) if respond_to?(:initial_status_acknowledged)
|
|
114
115
|
else
|
|
115
116
|
log_acknowledgement_for_unknown message
|
|
116
117
|
end
|
|
@@ -124,6 +125,7 @@ module RSMP
|
|
|
124
125
|
log_acknowledgement_for_original message, original
|
|
125
126
|
@acknowledgements[original.m_id] = message
|
|
126
127
|
@acknowledgement_condition.signal message
|
|
128
|
+
initial_status_acknowledged(original) if respond_to?(:initial_status_acknowledged)
|
|
127
129
|
else
|
|
128
130
|
log_acknowledgement_for_unknown message
|
|
129
131
|
end
|
|
@@ -26,11 +26,12 @@ module RSMP
|
|
|
26
26
|
def process_packet(json)
|
|
27
27
|
attributes = Message.parse_attributes(json)
|
|
28
28
|
message = Message.build(attributes, json)
|
|
29
|
+
message.attributes['mId'] = attributes['mId'] if message.is_a?(Version)
|
|
29
30
|
process_incoming_message(message)
|
|
30
31
|
rescue InvalidPacket => e
|
|
31
32
|
reject_invalid_packet(json, e)
|
|
32
33
|
rescue MalformedMessage => e
|
|
33
|
-
|
|
34
|
+
reject_malformed_version_or_packet(json, attributes, e)
|
|
34
35
|
rescue PeerMessageError => e
|
|
35
36
|
reject_processed_message(message, e)
|
|
36
37
|
rescue HandshakeError, FatalError => e
|
|
@@ -39,7 +40,28 @@ module RSMP
|
|
|
39
40
|
@node&.clear_deferred
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def reject_malformed_version_or_packet(json, attributes, error)
|
|
44
|
+
if attributes.is_a?(Hash) && attributes['type'] == 'Version' && core_3_3_0_or_newer?
|
|
45
|
+
message = Version.new(attributes)
|
|
46
|
+
message.attributes['mId'] = attributes['mId']
|
|
47
|
+
reject_fatal_message(message, error)
|
|
48
|
+
else
|
|
49
|
+
reject_malformed_packet(json, attributes, error)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_core_3_3_permissions!(message)
|
|
54
|
+
if core_3_3_0_or_newer? && message.is_a?(Alarm) && !use_alarms?
|
|
55
|
+
raise MessageRejected,
|
|
56
|
+
'Alarm exchange is disabled'
|
|
57
|
+
end
|
|
58
|
+
return unless core_3_3_0_or_newer? && @version_determined && !ready? && application_message?(message)
|
|
59
|
+
|
|
60
|
+
raise MessageRejected, 'Communication establishment is incomplete'
|
|
61
|
+
end
|
|
62
|
+
|
|
42
63
|
def process_incoming_message(message)
|
|
64
|
+
check_core_3_3_permissions!(message)
|
|
43
65
|
validate = should_validate_ingoing_message?(message)
|
|
44
66
|
if validate
|
|
45
67
|
validation = message.validate(schemas)
|
|
@@ -86,6 +108,7 @@ module RSMP
|
|
|
86
108
|
publish_peer_failure(failure, message: message)
|
|
87
109
|
log(text, message: message, level: :warning)
|
|
88
110
|
dont_acknowledge(message, text, reason)
|
|
111
|
+
complete_initial_status(message) if respond_to?(:complete_initial_status)
|
|
89
112
|
Result.failure(failure: failure)
|
|
90
113
|
end
|
|
91
114
|
|
|
@@ -109,11 +132,18 @@ module RSMP
|
|
|
109
132
|
text = "Rejected #{message&.type || 'message'}: #{error.message}"
|
|
110
133
|
failure = peer_failure(:protocol_failure, text, message: message)
|
|
111
134
|
publish_peer_failure(failure, message: message)
|
|
112
|
-
|
|
135
|
+
if message && (!message.is_a?(Version) || valid_received_message_id?(message))
|
|
136
|
+
dont_acknowledge(message, "Rejected #{message.type}", error.message.to_s)
|
|
137
|
+
end
|
|
113
138
|
close(reason: :protocol_failure, failure: failure)
|
|
114
139
|
Result.failure(failure: failure)
|
|
115
140
|
end
|
|
116
141
|
|
|
142
|
+
def valid_received_message_id?(message)
|
|
143
|
+
id = message.m_id
|
|
144
|
+
id.is_a?(String) && /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i.match?(id)
|
|
145
|
+
end
|
|
146
|
+
|
|
117
147
|
def peer_failure(code, text, message: nil, **context)
|
|
118
148
|
Failure.new(
|
|
119
149
|
code: code,
|
|
@@ -38,8 +38,24 @@ module RSMP
|
|
|
38
38
|
write_protocol(message)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def core_3_3_send_failure(message)
|
|
42
|
+
return unless core_3_3_0_or_newer?
|
|
43
|
+
return 'Alarm exchange is disabled' if message.is_a?(Alarm) && !use_alarms?
|
|
44
|
+
|
|
45
|
+
'Communication establishment is incomplete' if !ready? && application_message?(message)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def nts_attributes?(message)
|
|
49
|
+
message.attributes.key?('cId') || message.is_a?(Alarm) ||
|
|
50
|
+
message.is_a?(AggregatedStatus) || message.is_a?(AggregatedStatusRequest)
|
|
51
|
+
end
|
|
52
|
+
|
|
41
53
|
def prepare_message(message, validate:)
|
|
54
|
+
permission = core_3_3_send_failure(message)
|
|
55
|
+
return invalid_outbound_result(message, permission) if permission
|
|
56
|
+
|
|
42
57
|
message.direction = :out
|
|
58
|
+
apply_nts_message_attributes(message) if core_3_3_0_or_newer? && nts_attributes?(message)
|
|
43
59
|
message.encode_for(schemas) unless validate == false
|
|
44
60
|
message.generate_json
|
|
45
61
|
unless validate == false
|
|
@@ -135,10 +151,14 @@ module RSMP
|
|
|
135
151
|
end
|
|
136
152
|
|
|
137
153
|
def apply_nts_message_attributes(message)
|
|
138
|
-
|
|
154
|
+
if core_3_3_0_or_newer?
|
|
155
|
+
message.attributes['ntsOId'] = ''
|
|
156
|
+
message.attributes['xNId'] = ''
|
|
157
|
+
return
|
|
158
|
+
end
|
|
139
159
|
|
|
140
|
-
message.attributes['ntsOId'] = main
|
|
141
|
-
message.attributes['xNId'] = main
|
|
160
|
+
message.attributes['ntsOId'] = main&.ntsoid || ''
|
|
161
|
+
message.attributes['xNId'] = main&.xnid || ''
|
|
142
162
|
end
|
|
143
163
|
end
|
|
144
164
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
class Proxy
|
|
3
|
+
module Modules
|
|
4
|
+
# Shape and cross-message constraints for version negotiation.
|
|
5
|
+
module VersionValidation
|
|
6
|
+
def validate_version_array!(message)
|
|
7
|
+
items = message.attributes['RSMP']
|
|
8
|
+
valid = items.is_a?(Array) && !items.empty? && items.all? do |item|
|
|
9
|
+
item.is_a?(Hash) && item['vers'].is_a?(String) &&
|
|
10
|
+
(RSMP::Schema.strict_version?(item['vers']) || RSMP::Schema.normalize_core_version(item['vers']))
|
|
11
|
+
end
|
|
12
|
+
raise HandshakeError, 'Invalid RSMP version array' unless valid
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate_version!(message, step:)
|
|
16
|
+
validation = message.validate(core: core_version)
|
|
17
|
+
raise HandshakeError, validation.message if validation.invalid?
|
|
18
|
+
return unless core_3_3_0_or_newer?
|
|
19
|
+
|
|
20
|
+
raise HandshakeError, "Expected Version #{step}" unless message.step == step
|
|
21
|
+
|
|
22
|
+
validate_sxl_names!(message.sxls)
|
|
23
|
+
validate_primary_sxl!(message) if step == 'Request'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate_sxl_names!(items)
|
|
27
|
+
names = items.map { |item| item['name'] }
|
|
28
|
+
raise HandshakeError, 'Duplicate SXL names' unless names.uniq == names
|
|
29
|
+
|
|
30
|
+
items.each do |item|
|
|
31
|
+
version = item['version']
|
|
32
|
+
if version && !RSMP::Schema.strict_version?(version)
|
|
33
|
+
raise HandshakeError, 'SXL version must use MAJOR.MINOR.PATCH'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_primary_sxl!(message)
|
|
39
|
+
primary = message.attributes['SXL']
|
|
40
|
+
return unless primary && !primary.empty? && message.sxls.none? { |item| item['version'] == primary }
|
|
41
|
+
|
|
42
|
+
raise HandshakeError, 'Primary SXL version does not match SXLS'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_sxl_response!(sxls)
|
|
46
|
+
offered = sxl_request_items
|
|
47
|
+
names = offered.map { |item| item['name'] }
|
|
48
|
+
returned = sxls.reject { |item| item['status'] == 'expected' }.map { |item| item['name'] }
|
|
49
|
+
raise HandshakeError, 'SXLS must report every offered SXL exactly once' unless returned.sort == names.sort
|
|
50
|
+
|
|
51
|
+
sxls.each { |item| validate_sxl_status!(item, names) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_sxl_status!(item, names)
|
|
55
|
+
case item['status']
|
|
56
|
+
when 'ok'
|
|
57
|
+
raise HandshakeError, 'SXL version must match the request exactly' unless valid_sxl_response?(item)
|
|
58
|
+
when 'expected'
|
|
59
|
+
raise HandshakeError, 'Expected SXL was offered' if names.include?(item['name'])
|
|
60
|
+
when 'mismatch'
|
|
61
|
+
supported = item['supported']
|
|
62
|
+
unless supported == supported.sort_by { |version| Gem::Version.new(version) }
|
|
63
|
+
raise HandshakeError, 'Supported SXL versions must be sorted'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def valid_sxl_response?(accepted)
|
|
69
|
+
requested = sxl_request_items.find { |item| item['name'] == accepted['name'] }
|
|
70
|
+
RSMP::Schema.strict_version?(accepted['version']) && requested &&
|
|
71
|
+
requested['version'] == accepted['version']
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|