rsmp 0.52.0 → 0.53.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/documentation/configuration.md +53 -2
  5. data/lib/rsmp/component/component_base.rb +10 -1
  6. data/lib/rsmp/component/component_proxy.rb +8 -5
  7. data/lib/rsmp/component/components.rb +24 -2
  8. data/lib/rsmp/node/site/site.rb +21 -7
  9. data/lib/rsmp/node/site/status.rb +39 -0
  10. data/lib/rsmp/node/supervisor/modules/connection.rb +20 -4
  11. data/lib/rsmp/node/supervisor/modules/sites.rb +2 -0
  12. data/lib/rsmp/options/schemas/required_settings.json +20 -12
  13. data/lib/rsmp/options/schemas/supervisor_site.json +1 -1
  14. data/lib/rsmp/options/site_options.rb +1 -0
  15. data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
  16. data/lib/rsmp/proxy/modules/receive.rb +32 -2
  17. data/lib/rsmp/proxy/modules/send.rb +23 -3
  18. data/lib/rsmp/proxy/modules/version_validation.rb +76 -0
  19. data/lib/rsmp/proxy/modules/versions.rb +21 -22
  20. data/lib/rsmp/proxy/proxy.rb +21 -3
  21. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +22 -2
  22. data/lib/rsmp/proxy/site/modules/commands.rb +1 -1
  23. data/lib/rsmp/proxy/site/site_proxy.rb +22 -13
  24. data/lib/rsmp/proxy/site/sxl_selection.rb +13 -22
  25. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +56 -1
  26. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +11 -4
  27. data/lib/rsmp/proxy/supervisor/modules/commands.rb +4 -3
  28. data/lib/rsmp/proxy/supervisor/modules/message_buffer.rb +6 -1
  29. data/lib/rsmp/proxy/supervisor/modules/status.rb +46 -1
  30. data/lib/rsmp/proxy/supervisor/modules/status_updates.rb +17 -1
  31. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +12 -16
  32. data/lib/rsmp/schema/validation.rb +2 -1
  33. data/lib/rsmp/version.rb +1 -1
  34. data/schemas/core/3.1.2/definitions.json +5 -0
  35. data/schemas/core/3.1.2/status_subscribe.json +1 -1
  36. data/schemas/core/3.1.3/definitions.json +5 -0
  37. data/schemas/core/3.1.3/status_subscribe.json +1 -1
  38. data/schemas/core/3.1.4/definitions.json +5 -0
  39. data/schemas/core/3.1.4/status_subscribe.json +1 -1
  40. data/schemas/core/3.1.5/definitions.json +5 -0
  41. data/schemas/core/3.1.5/status_subscribe.json +1 -1
  42. data/schemas/core/3.2.0/definitions.json +5 -0
  43. data/schemas/core/3.2.0/status_subscribe.json +1 -1
  44. data/schemas/core/3.2.1/definitions.json +5 -0
  45. data/schemas/core/3.2.1/status_subscribe.json +1 -1
  46. data/schemas/core/3.2.2/definitions.json +5 -0
  47. data/schemas/core/3.2.2/status_subscribe.json +1 -1
  48. data/schemas/core/3.3.0/aggregated_status.json +17 -11
  49. data/schemas/core/3.3.0/aggregated_status_request.json +4 -4
  50. data/schemas/core/3.3.0/component_list.json +1 -1
  51. data/schemas/core/3.3.0/definitions.json +23 -2
  52. data/schemas/core/3.3.0/status_subscribe.json +14 -4
  53. data/schemas/core/3.3.0/version.json +46 -26
  54. data/schemas/tlc/1.0.10/defs/definitions.json +5 -0
  55. data/schemas/tlc/1.0.13/defs/definitions.json +5 -0
  56. data/schemas/tlc/1.0.14/defs/definitions.json +5 -0
  57. data/schemas/tlc/1.0.15/defs/definitions.json +5 -0
  58. data/schemas/tlc/1.0.7/defs/definitions.json +5 -0
  59. data/schemas/tlc/1.0.8/defs/definitions.json +5 -0
  60. data/schemas/tlc/1.0.9/defs/definitions.json +5 -0
  61. data/schemas/tlc/1.1.0/defs/definitions.json +5 -0
  62. data/schemas/tlc/1.2.0/defs/definitions.json +5 -0
  63. data/schemas/tlc/1.2.1/defs/definitions.json +5 -0
  64. data/schemas/tlc/1.3.0/defs/definitions.json +7 -0
  65. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfba3255efe256eae701725584e74d80a7ba66fe94708ef9bdc76cbd678c7700
4
- data.tar.gz: b0aebcd26383d1ed77fbcdd18351fecc0a7afcc962d582f17a3f488a53aaeb22
3
+ metadata.gz: 753bf9789b6bf177b83fb54c6fb95856b5f6ceed515a9ff17ba4993109a88d29
4
+ data.tar.gz: 0ab04b23c2e57490c605f26bb2962d79a88dc6a1e4b165085dedbc4dab52a834
5
5
  SHA512:
6
- metadata.gz: f7796d54dbed77a9d230a4e49f3ddadd22b3833b29fa05c882f9d7a6c540ed64ae55745f5b8dbe38a9fe095fa49b45684f9ebaf0a954d9fa021d892f2bdc5cec
7
- data.tar.gz: 5bf194b21059fd35d82e41093c68658585fe083cc061f5759e98d95df1f45e5fbd0958051cb3dbac8b771f915fc0511c056f72c887339f1b5df6c60f73d6a57f
6
+ metadata.gz: e66c0016019dff11724b0989dd3da9d319aa8a24ce8ad3b326068bd03abe73580d212c15023470378cc21690b92cb298666fde785195b1267e28a3b4fe5cbdbd
7
+ data.tar.gz: ccd8d17f4eef01bf389f7b17851ffc321b3d64bf2b562e06700809e693065f9b71bac39bc50fb6630a896d1e7fb1ea60a59e7fc6f0182e1cef36e9bb41582310
data/CHANGELOG.md CHANGED
@@ -735,3 +735,9 @@ 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.
741
+
742
+ ## 0.53.1
743
+ - skip repeated status value checks for buffered values (`q=old`) and unknown subscription settings
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.52.0)
4
+ rsmp (0.53.1)
5
5
  async (~> 2.45)
6
6
  colorize (~> 1.1)
7
7
  io-endpoint (~> 0.17)
@@ -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
- - Configured components must include exactly one `main` component. Sites supply a default main
25
- component when omitted; supervisor site settings can inherit it from `default`.
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
- return unless input != @aggregated_status
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)
@@ -18,8 +18,8 @@ module RSMP
18
18
  end
19
19
  end
20
20
 
21
- # Check that were not receiving repeated update values.
22
- # The check is not performed for item with an update interval.
21
+ # Check live values only for known change-only subscriptions. Buffered
22
+ # values may have been generated under different subscription settings.
23
23
  def check_repeat_values(message, subscription_list)
24
24
  message.attribute('sS').each do |item|
25
25
  check_status_item_for_repeats(item, subscription_list)
@@ -27,6 +27,8 @@ module RSMP
27
27
  end
28
28
 
29
29
  def check_status_item_for_repeats(item, subscription_list)
30
+ return if item['q'] == 'old'
31
+
30
32
  status_code = item['sCI']
31
33
  status_name = item['n']
32
34
  return if update_rate_set?(subscription_list, status_code, status_name)
@@ -39,12 +41,13 @@ module RSMP
39
41
 
40
42
  def update_rate_set?(subscription_list, status_code, status_name)
41
43
  urt = subscription_list.dig(c_id, status_code, status_name, 'uRt')
42
- urt.to_i.positive?
44
+ urt.to_f.positive?
43
45
  end
44
46
 
45
47
  def should_check_repeats?(subscription_list, status_code, status_name)
46
- soc = subscription_list.dig(c_id, status_code, status_name, 'sOc')
47
- return false if soc == false
48
+ subscription = subscription_list.dig(c_id, status_code, status_name)
49
+ return false unless subscription
50
+ return false if subscription['sOc'] == false
48
51
  return false if @allow_repeat_updates[status_code]&.include?(status_name)
49
52
 
50
53
  true
@@ -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
 
@@ -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
- proxy.send_generated_message alarm if proxy.receive_alarms?
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.attribute('siteId').first['sId']
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, protocol)
132
+ def validate_and_start_proxy(proxy, _protocol)
116
133
  proxy.setup_site_settings
117
- proxy.check_core_version peek_version_message(protocol)
118
- log "Validating using core version #{proxy.core_version}", level: :debug
134
+
119
135
  proxy.start
120
136
  proxy.wait
121
137
  ensure
@@ -68,6 +68,8 @@ module RSMP
68
68
  @site_id_condition.signal
69
69
  end
70
70
 
71
+ def site_aggregated_status_changed(site_proxy); end
72
+
71
73
  def aggregated_status_changed(site_proxy, component); end
72
74
  end
73
75
  end
@@ -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
- "receive_alarms": { "type": "boolean" },
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
- reject_malformed_packet(json, attributes, e)
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
- dont_acknowledge(message, "Rejected #{message.type}", error.message.to_s) if message
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
- return if core_3_3?
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 && main.ntsoid ? main.ntsoid : ''
141
- message.attributes['xNId'] = main && main.xnid ? main.xnid : ''
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