rsmp 0.37.0 → 0.39.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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +22 -0
  3. data/.github/workflows/rubocop.yaml +17 -0
  4. data/.gitignore +5 -6
  5. data/.rubocop.yml +69 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +14 -1
  8. data/Gemfile.lock +64 -29
  9. data/Rakefile +3 -3
  10. data/lib/rsmp/cli.rb +148 -124
  11. data/lib/rsmp/collect/ack_collector.rb +8 -7
  12. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  13. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  14. data/lib/rsmp/collect/alarm_matcher.rb +6 -6
  15. data/lib/rsmp/collect/collector/logging.rb +18 -0
  16. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  17. data/lib/rsmp/collect/collector/status.rb +34 -0
  18. data/lib/rsmp/collect/collector.rb +69 -150
  19. data/lib/rsmp/collect/command_matcher.rb +19 -6
  20. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  21. data/lib/rsmp/collect/distributor.rb +14 -11
  22. data/lib/rsmp/collect/filter.rb +31 -15
  23. data/lib/rsmp/collect/matcher.rb +9 -13
  24. data/lib/rsmp/collect/queue.rb +7 -7
  25. data/lib/rsmp/collect/receiver.rb +11 -15
  26. data/lib/rsmp/collect/state_collector.rb +116 -77
  27. data/lib/rsmp/collect/status_collector.rb +6 -6
  28. data/lib/rsmp/collect/status_matcher.rb +15 -4
  29. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -38
  30. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  31. data/lib/rsmp/component/component_base.rb +88 -0
  32. data/lib/rsmp/component/component_proxy.rb +75 -0
  33. data/lib/rsmp/component/components.rb +62 -0
  34. data/lib/rsmp/convert/export/json_schema.rb +118 -110
  35. data/lib/rsmp/convert/import/yaml.rb +22 -18
  36. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +8 -11
  37. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +3 -1
  38. data/lib/rsmp/helpers/error.rb +72 -0
  39. data/lib/rsmp/helpers/inspect.rb +41 -0
  40. data/lib/rsmp/log/archive.rb +97 -0
  41. data/lib/rsmp/log/colorization.rb +41 -0
  42. data/lib/rsmp/log/filtering.rb +54 -0
  43. data/lib/rsmp/log/logger.rb +207 -0
  44. data/lib/rsmp/{logging.rb → log/logging.rb} +6 -7
  45. data/lib/rsmp/message.rb +185 -148
  46. data/lib/rsmp/{node.rb → node/node.rb} +20 -19
  47. data/lib/rsmp/{protocol.rb → node/protocol.rb} +6 -3
  48. data/lib/rsmp/node/site/site.rb +192 -0
  49. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  50. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  51. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  52. data/lib/rsmp/node/supervisor/supervisor.rb +69 -0
  53. data/lib/rsmp/{task.rb → node/task.rb} +13 -14
  54. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  55. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  56. data/lib/rsmp/proxy/modules/send.rb +76 -0
  57. data/lib/rsmp/proxy/modules/state.rb +25 -0
  58. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  59. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  60. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  61. data/lib/rsmp/proxy/proxy.rb +197 -0
  62. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  63. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  64. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  65. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  66. data/lib/rsmp/proxy/site/site_proxy.rb +204 -0
  67. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  68. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  69. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  70. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  71. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +177 -0
  72. data/lib/rsmp/tlc/detector_logic.rb +19 -34
  73. data/lib/rsmp/tlc/input_states.rb +126 -0
  74. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  75. data/lib/rsmp/tlc/modules/display.rb +78 -0
  76. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  77. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  78. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  79. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  80. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  81. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  82. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  83. data/lib/rsmp/tlc/modules/system.rb +140 -0
  84. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  85. data/lib/rsmp/tlc/signal_group.rb +38 -41
  86. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  87. data/lib/rsmp/tlc/signal_priority.rb +40 -35
  88. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  89. data/lib/rsmp/tlc/traffic_controller.rb +38 -1010
  90. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  91. data/lib/rsmp/version.rb +1 -1
  92. data/lib/rsmp.rb +82 -48
  93. data/rsmp.gemspec +24 -31
  94. metadata +79 -139
  95. data/lib/rsmp/archive.rb +0 -76
  96. data/lib/rsmp/collect/message_matchers.rb +0 -0
  97. data/lib/rsmp/component_base.rb +0 -87
  98. data/lib/rsmp/component_proxy.rb +0 -57
  99. data/lib/rsmp/components.rb +0 -65
  100. data/lib/rsmp/error.rb +0 -71
  101. data/lib/rsmp/inspect.rb +0 -46
  102. data/lib/rsmp/logger.rb +0 -216
  103. data/lib/rsmp/proxy.rb +0 -693
  104. data/lib/rsmp/site.rb +0 -188
  105. data/lib/rsmp/site_proxy.rb +0 -389
  106. data/lib/rsmp/supervisor.rb +0 -302
  107. data/lib/rsmp/supervisor_proxy.rb +0 -510
  108. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -0,0 +1,72 @@
1
+ module RSMP
2
+ class Error < StandardError
3
+ end
4
+
5
+ class InvalidPacket < Error
6
+ end
7
+
8
+ class MalformedMessage < Error
9
+ end
10
+
11
+ # Raised when schema validation fails.
12
+ class SchemaError < Error
13
+ attr_accessor :schemas
14
+ end
15
+
16
+ class InvalidMessage < Error
17
+ end
18
+
19
+ class UnknownMessage < Error
20
+ end
21
+
22
+ class MissingAcknowledgment < Error
23
+ end
24
+
25
+ class MissingWatchdog < Error
26
+ end
27
+
28
+ class MessageRejected < Error
29
+ end
30
+
31
+ class MissingAttribute < InvalidMessage
32
+ end
33
+
34
+ class FatalError < Error
35
+ end
36
+
37
+ class HandshakeError < FatalError
38
+ end
39
+
40
+ class NotReady < Error
41
+ end
42
+
43
+ class TimeoutError < Error
44
+ end
45
+
46
+ class DisconnectError < Error
47
+ end
48
+
49
+ class ConnectionError < Error
50
+ end
51
+
52
+ class UnknownComponent < Error
53
+ end
54
+
55
+ class UnknownCommand < Error
56
+ end
57
+
58
+ class UnknownStatus < Error
59
+ end
60
+
61
+ class ConfigurationError < Error
62
+ end
63
+
64
+ class RepeatedAlarmError < Error
65
+ end
66
+
67
+ class RepeatedStatusError < Error
68
+ end
69
+
70
+ class TimestampError < Error
71
+ end
72
+ end
@@ -0,0 +1,41 @@
1
+ module RSMP
2
+ # Custom inspect, to reduce noise
3
+ #
4
+ # Instance variables of classes starting with Async or RSMP are shown
5
+ # with only their class name and object id, which reduces output,
6
+ # especially for deep object structures.
7
+ # Additionally, a list of variables to shown in short format can be passed.
8
+ #
9
+ # The short form is generated by using to_s() insted of inspect()
10
+ #
11
+ # Array#to_s and Hash#to_s usually show items, but here we show just number
12
+ # of items, when the short form is requested.
13
+ module Inspect
14
+ def inspector *short_items
15
+ instance_variables.map do |var_name|
16
+ var = instance_variable_get(var_name)
17
+ class_name = var.class.name
18
+
19
+ short = short_items.include?(var_name) ||
20
+ class_name.start_with?('Async') ||
21
+ class_name.start_with?('RSMP')
22
+
23
+ if short
24
+ if var.is_a?(Array) || var.is_a?(Hash)
25
+ "#{var_name}: #<#{class_name}:#{class_name.object_id}, #{var.size} items>"
26
+ else
27
+ "#{var_name}: #{var}"
28
+ end
29
+ else
30
+ "#{var_name}: #{var.inspect}"
31
+ end
32
+ end.join(', ')
33
+ end
34
+
35
+ # override this if you want additional variable to be shown in the short format,
36
+ # or ottherwise change the inspect format
37
+ def inspect
38
+ "#<#{self.class.name}:#{object_id}, #{inspector}>"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,97 @@
1
+ module RSMP
2
+ # Archive of log items, which can be messages or info items.
3
+ # All items are timestamped, and stored chronologically.
4
+ class Archive
5
+ include Inspect
6
+
7
+ attr_reader :items
8
+
9
+ @index = 0
10
+
11
+ class << self
12
+ attr_accessor :index
13
+ end
14
+
15
+ def initialize(max = 100)
16
+ @items = []
17
+ @max = max
18
+ end
19
+
20
+ def inspect
21
+ "#<#{self.class.name}:#{object_id}, #{inspector(:@items)}>"
22
+ end
23
+
24
+ def self.prepare_item(item)
25
+ raise ArgumentError unless item.is_a? Hash
26
+
27
+ cleaned = item.slice(:author, :level, :ip, :port, :site_id, :component, :text, :message, :exception)
28
+ cleaned[:timestamp] = Clock.now
29
+ if item[:message]
30
+ cleaned[:direction] = item[:message].direction
31
+ cleaned[:component] = item[:message].attributes['cId']
32
+ end
33
+
34
+ cleaned
35
+ end
36
+
37
+ def self.increase_index
38
+ self.index += 1
39
+ end
40
+
41
+ def self.current_index
42
+ index
43
+ end
44
+
45
+ def by_level(levels)
46
+ items.select { |item| levels.include? item[:level] }
47
+ end
48
+
49
+ def strings
50
+ items.map { |item| item[:str] }
51
+ end
52
+
53
+ def add(item)
54
+ item[:index] = RSMP::Archive.increase_index
55
+ @items << item
56
+ return unless @items.size > @max
57
+
58
+ @items.shift
59
+ end
60
+
61
+ private
62
+
63
+ def find(options, &)
64
+ # search backwards from newest to older, stopping once messages
65
+ # are older that options[:earliest]
66
+ out = []
67
+ @items.reverse_each do |item|
68
+ break if too_old?(item, options[:earliest])
69
+ next unless matches_filters?(item, options, &)
70
+
71
+ out.unshift item
72
+ end
73
+ out
74
+ end
75
+
76
+ def too_old?(item, earliest)
77
+ earliest && item[:timestamp] < earliest
78
+ end
79
+
80
+ def matches_filters?(item, options, &block)
81
+ return false if options[:level] && item[:level] != options[:level]
82
+ return false if options[:type] && !matches_type?(item, options[:type])
83
+ return false if options[:with_message] && !message?(item)
84
+ return false if block_given? && block.call != true
85
+
86
+ true
87
+ end
88
+
89
+ def matches_type?(item, type)
90
+ item[:message] && item[:message].type == type
91
+ end
92
+
93
+ def message?(item)
94
+ item[:direction] && item[:message]
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ module RSMP
2
+ class Logger
3
+ # Handles colorization of log output
4
+ module Colorization
5
+ def default_colors
6
+ {
7
+ 'info' => 'white',
8
+ 'log' => 'light_blue',
9
+ 'statistics' => 'light_black',
10
+ 'warning' => 'light_yellow',
11
+ 'error' => 'red',
12
+ 'debug' => 'light_black',
13
+ 'collect' => 'light_black'
14
+ }
15
+ end
16
+
17
+ def colorize_with_map(level, str, colors)
18
+ color = colors[level.to_s]
19
+ color ? str.colorize(color.to_sym) : str
20
+ end
21
+
22
+ def apply_hash_colors(level, str)
23
+ colors = default_colors
24
+ colors.merge! @settings['color'] if @settings['color'].is_a?(Hash)
25
+ colorize_with_map(level, str, colors)
26
+ end
27
+
28
+ def colorize(level, str)
29
+ return str if @settings['color'] == false || @settings['color'].nil?
30
+
31
+ if @settings['color'] == true || @settings['color'].is_a?(Hash)
32
+ apply_hash_colors(level, str)
33
+ elsif %i[nack warning error].include?(level)
34
+ str.colorize(@settings['color']).bold
35
+ else
36
+ str.colorize @settings['color']
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ module RSMP
2
+ class Logger
3
+ # Handles filtering logic for log output
4
+ module Filtering
5
+ def level_enabled?(item)
6
+ return false if @settings['info'] == false && item[:level] == :info
7
+ return false if @settings['debug'] != true && item[:level] == :debug
8
+ return false if @settings['statistics'] != true && item[:level] == :statistics
9
+ return false if @settings['test'] != true && item[:level] == :test
10
+
11
+ true
12
+ end
13
+
14
+ def message_ignored?(item)
15
+ return false unless item[:message]
16
+
17
+ type = item[:message].type
18
+ ack = %w[MessageAck MessageNotAck].include?(type)
19
+ return true unless ignorable?(type, ack, item)
20
+ return true unless acknowledgement_enabled?(ack, item)
21
+
22
+ false
23
+ end
24
+
25
+ def ignorable?(type, ack, item)
26
+ @ignorable.each_pair do |key, types|
27
+ ignore = [types].flatten
28
+ next unless @settings[key] == false
29
+ return false if ignore.include?(type)
30
+
31
+ return false if ack && item[:message].original && ignore.include?(item[:message].original.type)
32
+ end
33
+ true
34
+ end
35
+
36
+ def acknowledgement_enabled?(ack, item)
37
+ return true unless ack
38
+ return true if @settings['acknowledgements'] != false
39
+ return true if %i[not_acknowledged warning error].include?(item[:level])
40
+
41
+ false
42
+ end
43
+
44
+ def output?(item, force: false)
45
+ return false if muted?(item)
46
+ return false if @settings['active'] == false && force != true
47
+ return false unless level_enabled?(item)
48
+ return false if message_ignored?(item)
49
+
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,207 @@
1
+ module RSMP
2
+ # Formats and outputs log messages according to configured settings.
3
+ class Logger
4
+ include Filtering
5
+ include Colorization
6
+
7
+ attr_accessor :settings
8
+
9
+ def default_output_settings
10
+ {
11
+ 'active' => true,
12
+ 'path' => nil,
13
+ 'stream' => nil,
14
+ 'color' => true,
15
+ 'debug' => false,
16
+ 'statistics' => false,
17
+ 'hide_ip_and_port' => false,
18
+ 'acknowledgements' => false,
19
+ 'watchdogs' => false,
20
+ 'alarms' => true,
21
+ 'json' => false,
22
+ 'tabs' => '-'
23
+ }
24
+ end
25
+
26
+ def default_field_settings
27
+ {
28
+ 'prefix' => false,
29
+ 'index' => false,
30
+ 'author' => false,
31
+ 'timestamp' => true,
32
+ 'ip' => false,
33
+ 'port' => false,
34
+ 'site_id' => true,
35
+ 'component' => true,
36
+ 'direction' => false,
37
+ 'level' => false,
38
+ 'id' => true,
39
+ 'text' => true
40
+ }
41
+ end
42
+
43
+ def default_logger_settings
44
+ default_output_settings.merge(default_field_settings)
45
+ end
46
+
47
+ def default_field_lengths
48
+ {
49
+ 'index' => 7,
50
+ 'author' => 13,
51
+ 'timestamp' => 24,
52
+ 'ip' => 22,
53
+ 'port' => 5,
54
+ 'site_id' => 19,
55
+ 'component' => 19,
56
+ 'direction' => 3,
57
+ 'level' => 7,
58
+ 'id' => 4
59
+ }
60
+ end
61
+
62
+ def ignorable_messages
63
+ {
64
+ 'versions' => ['Version'],
65
+ 'statuses' => %w[StatusRequest StatusSubscribe StatusUnsubscribe StatusResponse StatusUpdate],
66
+ 'commands' => %w[CommandRequest CommandResponse],
67
+ 'watchdogs' => 'Watchdog',
68
+ 'alarms' => ['Alarm'],
69
+ 'aggregated_status' => %w[AggregatedStatus AggregatedStatusRequest]
70
+ }
71
+ end
72
+
73
+ def apply_default_lengths(settings)
74
+ lengths = default_field_lengths
75
+ settings.to_h do |key, value|
76
+ if value == true && lengths[key]
77
+ [key, lengths[key]]
78
+ else
79
+ [key, value]
80
+ end
81
+ end
82
+ end
83
+
84
+ def initialize(settings = {})
85
+ @ignorable = ignorable_messages
86
+ @settings = settings ? default_logger_settings.merge(settings) : default_logger_settings
87
+ @settings = apply_default_lengths(@settings)
88
+ @muted = {}
89
+ setup_output_destination
90
+ end
91
+
92
+ def setup_output_destination
93
+ @stream = if @settings['stream']
94
+ @settings['stream']
95
+ elsif @settings['path']
96
+ File.open(@settings['path'], 'a') # appending
97
+ else
98
+ $stdout
99
+ end
100
+ end
101
+
102
+ def mute(ip, port)
103
+ @muted["#{ip}:#{port}"] = true
104
+ end
105
+
106
+ def unmute(ip, port)
107
+ @muted.delete "#{ip}:#{port}"
108
+ end
109
+
110
+ def unmute_all
111
+ @muted = {}
112
+ end
113
+
114
+ def muted?(item)
115
+ item[:ip] && item[:port] && @muted["#{item[:ip]}:#{item[:port]}"]
116
+ end
117
+
118
+ def level_enabled?(item)
119
+ return false if @settings['info'] == false && item[:level] == :info
120
+ return false if @settings['debug'] != true && item[:level] == :debug
121
+ return false if @settings['statistics'] != true && item[:level] == :statistics
122
+ return false if @settings['test'] != true && item[:level] == :test
123
+
124
+ true
125
+ end
126
+
127
+ def output(level, str)
128
+ return if str.empty? || /^\s+$/.match(str)
129
+
130
+ str = colorize level, str
131
+ @stream.puts str
132
+ @stream.flush
133
+ end
134
+
135
+ def log(item, force: false)
136
+ return unless output?(item, force: force)
137
+
138
+ output item[:level], build_output(item)
139
+ end
140
+
141
+ def self.shorten_message_id(m_id, length = 4)
142
+ if m_id
143
+ m_id[0..(length - 1)].ljust(length)
144
+ else
145
+ ' ' * length
146
+ end
147
+ end
148
+
149
+ def dump(archive, num: nil)
150
+ num ||= archive.items.size
151
+ log = archive.items.last(num).map do |item|
152
+ str = build_output item
153
+ colorize item[:level], str
154
+ end
155
+ log.join("\n")
156
+ end
157
+
158
+ def build_part(parts, item, key, &block)
159
+ skey = key.to_s
160
+ return unless @settings[skey]
161
+
162
+ part = item[key]
163
+ part = yield part if block
164
+ part = part.to_s
165
+ part = part.ljust @settings[skey] if @settings[skey].is_a?(Integer)
166
+
167
+ # replace the first char with a dash if string is all whitespace
168
+ part = @settings['tabs'].ljust(part.length) if @settings['tabs'] && part !~ /\S/
169
+ parts << part
170
+ end
171
+
172
+ def add_metadata_parts(parts, item)
173
+ build_part(parts, item, :prefix) { @settings['prefix'] if @settings['prefix'] != false }
174
+ build_part(parts, item, :index)
175
+ build_part(parts, item, :author)
176
+ build_part(parts, item, :timestamp) { |part| Clock.to_s part }
177
+ end
178
+
179
+ def add_connection_parts(parts, item)
180
+ build_part(parts, item, :ip)
181
+ build_part(parts, item, :port)
182
+ build_part(parts, item, :site_id)
183
+ build_part(parts, item, :component)
184
+ end
185
+
186
+ def add_message_parts(parts, item)
187
+ build_part(parts, item, :direction) { |part| { in: 'In', out: 'Out' }[part] }
188
+ build_part(parts, item, :level, &:capitalize)
189
+ build_part(parts, item, :id) { Logger.shorten_message_id(item[:message].m_id, 4) if item[:message] }
190
+ build_part(parts, item, :text)
191
+ build_part(parts, item, :json) { item[:message]&.json }
192
+ build_part(parts, item, :exception) { |e| [e.class, e.backtrace].flatten.join("\n") }
193
+ end
194
+
195
+ def add_output_parts(parts, item)
196
+ add_metadata_parts(parts, item)
197
+ add_connection_parts(parts, item)
198
+ add_message_parts(parts, item)
199
+ end
200
+
201
+ def build_output(item)
202
+ parts = []
203
+ add_output_parts(parts, item)
204
+ parts.join(' ').chomp(@settings['tabs'].to_s).rstrip
205
+ end
206
+ end
207
+ end
@@ -3,24 +3,23 @@
3
3
  #
4
4
 
5
5
  module RSMP
6
+ # Logging integration providing `archive` and `logger` helpers.
6
7
  module Logging
7
8
  attr_reader :archive, :logger
8
9
 
9
- def initialize_logging options
10
+ def initialize_logging(options)
10
11
  @archive = options[:archive] || RSMP::Archive.new
11
12
  @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
12
13
  end
13
14
 
14
- def author
15
- end
15
+ def author; end
16
16
 
17
- def log str, options={}
18
- default = { text:str, level: :log, author: author, ip: @ip, port: @port }
17
+ def log(str, options = {})
18
+ default = { text: str, level: :log, author: author, ip: @ip, port: @port }
19
19
  prepared = RSMP::Archive.prepare_item default.merge(options)
20
20
  @archive.add prepared
21
21
  @logger.log prepared
22
22
  prepared
23
23
  end
24
-
25
24
  end
26
- end
25
+ end