openc3 5.20.0 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +12 -120
  3. data/data/config/command_modifiers.yaml +13 -1
  4. data/data/config/interface_modifiers.yaml +21 -4
  5. data/data/config/item_modifiers.yaml +1 -1
  6. data/data/config/microservice.yaml +15 -2
  7. data/data/config/param_item_modifiers.yaml +1 -1
  8. data/data/config/parameter_modifiers.yaml +1 -1
  9. data/data/config/table_manager.yaml +2 -2
  10. data/data/config/target.yaml +11 -0
  11. data/data/config/telemetry_modifiers.yaml +17 -1
  12. data/data/config/tool.yaml +12 -0
  13. data/data/config/widgets.yaml +13 -17
  14. data/lib/openc3/accessors/form_accessor.rb +4 -3
  15. data/lib/openc3/accessors/html_accessor.rb +3 -3
  16. data/lib/openc3/accessors/http_accessor.rb +13 -13
  17. data/lib/openc3/accessors/xml_accessor.rb +16 -4
  18. data/lib/openc3/api/target_api.rb +0 -30
  19. data/lib/openc3/config/config_parser.rb +6 -3
  20. data/lib/openc3/core_ext/array.rb +0 -16
  21. data/lib/openc3/core_ext.rb +0 -1
  22. data/lib/openc3/interfaces/file_interface.rb +198 -0
  23. data/lib/openc3/interfaces/http_client_interface.rb +71 -39
  24. data/lib/openc3/interfaces/http_server_interface.rb +0 -7
  25. data/lib/openc3/interfaces/interface.rb +2 -0
  26. data/lib/openc3/interfaces/mqtt_interface.rb +32 -15
  27. data/lib/openc3/interfaces/mqtt_stream_interface.rb +19 -4
  28. data/lib/openc3/interfaces/protocols/crc_protocol.rb +7 -0
  29. data/lib/openc3/interfaces/serial_interface.rb +1 -0
  30. data/lib/openc3/interfaces.rb +2 -4
  31. data/lib/openc3/microservices/multi_microservice.rb +3 -3
  32. data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +31 -0
  33. data/lib/openc3/migrations/20241208080001_no_trigger_group.rb +46 -0
  34. data/lib/openc3/models/interface_model.rb +9 -3
  35. data/lib/openc3/models/microservice_model.rb +8 -1
  36. data/lib/openc3/models/plugin_model.rb +6 -1
  37. data/lib/openc3/models/python_package_model.rb +6 -1
  38. data/lib/openc3/models/reaction_model.rb +14 -10
  39. data/lib/openc3/models/scope_model.rb +60 -42
  40. data/lib/openc3/models/target_model.rb +17 -1
  41. data/lib/openc3/models/timeline_model.rb +17 -5
  42. data/lib/openc3/models/tool_model.rb +15 -3
  43. data/lib/openc3/models/trigger_group_model.rb +6 -3
  44. data/lib/openc3/operators/microservice_operator.rb +8 -0
  45. data/lib/openc3/packets/commands.rb +17 -6
  46. data/lib/openc3/packets/limits.rb +0 -12
  47. data/lib/openc3/packets/packet.rb +1 -1
  48. data/lib/openc3/packets/packet_item.rb +30 -36
  49. data/lib/openc3/packets/structure_item.rb +2 -2
  50. data/lib/openc3/script/script.rb +0 -10
  51. data/lib/openc3/script/web_socket_api.rb +2 -2
  52. data/lib/openc3/streams/mqtt_stream.rb +41 -33
  53. data/lib/openc3/streams/serial_stream.rb +27 -27
  54. data/lib/openc3/streams/stream.rb +17 -17
  55. data/lib/openc3/streams/tcpip_client_stream.rb +1 -1
  56. data/lib/openc3/streams/tcpip_socket_stream.rb +19 -19
  57. data/lib/openc3/system/system.rb +1 -1
  58. data/lib/openc3/system.rb +2 -3
  59. data/lib/openc3/tools/table_manager/table.rb +2 -2
  60. data/lib/openc3/tools/table_manager/table_parser.rb +1 -1
  61. data/lib/openc3/top_level.rb +0 -5
  62. data/lib/openc3/topics/command_decom_topic.rb +0 -7
  63. data/lib/openc3/utilities/bucket_utilities.rb +1 -1
  64. data/lib/openc3/utilities/cli_generator.rb +0 -1
  65. data/lib/openc3/version.rb +7 -7
  66. data/templates/plugin/README.md +1 -1
  67. data/templates/target/targets/TARGET/lib/target.rb +1 -1
  68. data/templates/tool_angular/package.json +8 -8
  69. data/templates/tool_angular/src/app/app.component.html +4 -13
  70. data/templates/tool_angular/src/app/app.component.scss +5 -13
  71. data/templates/tool_angular/src/app/app.component.ts +5 -4
  72. data/templates/tool_angular/src/app/custom-overlay-container.ts +2 -2
  73. data/templates/tool_angular/src/app/openc3-api.d.ts +1 -1
  74. data/templates/tool_angular/src/main.single-spa.ts +1 -1
  75. data/templates/tool_react/package.json +1 -0
  76. data/templates/tool_react/src/root.component.js +1 -1
  77. data/templates/tool_svelte/package.json +11 -9
  78. data/templates/tool_svelte/rollup.config.js +2 -0
  79. data/templates/tool_svelte/src/App.svelte +2 -2
  80. data/templates/tool_vue/eslint.config.mjs +68 -0
  81. data/templates/tool_vue/jsconfig.json +1 -1
  82. data/templates/tool_vue/package.json +26 -43
  83. data/templates/tool_vue/src/App.vue +3 -5
  84. data/templates/tool_vue/src/main.js +12 -23
  85. data/templates/tool_vue/src/router.js +19 -18
  86. data/templates/tool_vue/src/tools/tool_name/tool_name.vue +2 -2
  87. data/templates/tool_vue/vite.config.js +52 -0
  88. data/templates/widget/package.json +19 -26
  89. data/templates/widget/src/Widget.vue +13 -15
  90. data/templates/widget/vite.config.js +26 -0
  91. metadata +10 -41
  92. data/lib/openc3/core_ext/hash.rb +0 -40
  93. data/lib/openc3/core_ext/httpclient.rb +0 -11
  94. data/lib/openc3/interfaces/linc_interface.rb +0 -480
  95. data/lib/openc3/interfaces/protocols/override_protocol.rb +0 -4
  96. data/lib/openc3/microservices/critical_cmd_microservice.rb +0 -74
  97. data/lib/openc3/microservices/reaction_microservice.rb +0 -607
  98. data/lib/openc3/microservices/timeline_microservice.rb +0 -398
  99. data/lib/openc3/microservices/trigger_group_microservice.rb +0 -698
  100. data/lib/openc3/migrations/20230615000000_autonomic.rb +0 -86
  101. data/lib/openc3/migrations/20240915000000_activity_uuid.rb +0 -28
  102. data/lib/openc3/migrations/20241016000000_scope_critical_cmd.rb +0 -24
  103. data/lib/openc3/system/system_config.rb +0 -413
  104. data/templates/tool_svelte/src/services/api.js +0 -92
  105. data/templates/tool_svelte/src/services/axios.js +0 -85
  106. data/templates/tool_svelte/src/services/cable.js +0 -65
  107. data/templates/tool_svelte/src/services/config-parser.js +0 -198
  108. data/templates/tool_svelte/src/services/openc3-api.js +0 -606
  109. data/templates/tool_vue/.eslintrc.js +0 -43
  110. data/templates/tool_vue/babel.config.json +0 -11
  111. data/templates/tool_vue/vue.config.js +0 -38
  112. data/templates/widget/.eslintrc.js +0 -43
  113. data/templates/widget/babel.config.json +0 -11
  114. data/templates/widget/vue.config.js +0 -28
  115. /data/templates/tool_vue/{.prettierrc.js → .prettierrc.cjs} +0 -0
  116. /data/templates/widget/{.prettierrc.js → .prettierrc.cjs} +0 -0
@@ -1,74 +0,0 @@
1
- # encoding: ascii-8bit
2
-
3
- # Copyright 2022 OpenC3, Inc.
4
- # All Rights Reserved.
5
- #
6
- # This program is free software; you can modify and/or redistribute it
7
- # under the terms of the GNU Affero General Public License
8
- # as published by the Free Software Foundation; version 3 with
9
- # attribution addendums as found in the LICENSE.txt
10
- #
11
- # This program is distributed in the hope that it will be useful,
12
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- # GNU Affero General Public License for more details.
15
- #
16
- # This file may also be used under the terms of a commercial license
17
- # if purchased from OpenC3, Inc.
18
-
19
- require 'openc3/utilities/logger'
20
- require 'openc3/microservices/microservice'
21
- begin
22
- require 'openc3-enterprise/models/critical_cmd_model'
23
- rescue LoadError
24
- module OpenC3
25
- class CriticalCmdModel
26
- def self.get_all_models(scope:)
27
- []
28
- end
29
- end
30
- end
31
- end
32
-
33
- module OpenC3
34
- class CriticalCmdMicroservice < Microservice
35
- SLEEP_PERIOD_SECONDS = 3 # Check every 3 seconds
36
- TWENTY_FOUR_HOURS_NSEC = 24 * 60 * 60 * 1_000_000_000
37
-
38
- def run
39
- @run_sleeper = Sleeper.new
40
- critical_cmd_waiting = false
41
- while true
42
- models = CriticalCmdModel.get_all_models(scope: @scope)
43
- pre_waiting = critical_cmd_waiting
44
- critical_cmd_waiting = false
45
- old_time = Time.now.to_nsec_from_epoch - TWENTY_FOUR_HOURS_NSEC
46
- models.each do |name, model|
47
- # Cleanup older than 24 hours
48
- if model.updated_at < old_time
49
- model.destroy
50
- elsif model.status == 'WAITING'
51
- # Tell the frontend about critical commands pending
52
- critical_cmd_waiting = true
53
- data = Logger.build_log_data(Logger::INFO_LEVEL, "Critical Cmd Waiting", user: model.username, type: Logger::EPHEMERAL, other: {"uuid" => model.name, "cmd_string" => model.cmd_hash["cmd_string"]})
54
- EphemeralStoreQueued.write_topic("#{scope}__openc3_ephemeral_messages", data, '*', 100)
55
- end
56
- end
57
- if pre_waiting and not critical_cmd_waiting
58
- data = Logger.build_log_data(Logger::INFO_LEVEL, "All Critical Cmds Handled", type: Logger::EPHEMERAL)
59
- EphemeralStoreQueued.write_topic("#{scope}__openc3_ephemeral_messages", data, '*', 100)
60
- end
61
- @count += 1
62
- break if @cancel_thread
63
- break if @run_sleeper.sleep(SLEEP_PERIOD_SECONDS)
64
- end
65
- end
66
-
67
- def shutdown
68
- @run_sleeper.cancel if @run_sleeper
69
- super()
70
- end
71
- end
72
- end
73
-
74
- OpenC3::CriticalCmdMicroservice.run if __FILE__ == $0
@@ -1,607 +0,0 @@
1
- # encoding: ascii-8bit
2
-
3
- # Copyright 2022 Ball Aerospace & Technologies Corp.
4
- # All Rights Reserved.
5
- #
6
- # This program is free software; you can modify and/or redistribute it
7
- # under the terms of the GNU Affero General Public License
8
- # as published by the Free Software Foundation; version 3 with
9
- # attribution addendums as found in the LICENSE.txt
10
- #
11
- # This program is distributed in the hope that it will be useful,
12
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- # GNU Affero General Public License for more details.
15
-
16
- # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
18
- # All Rights Reserved
19
- #
20
- # This file may also be used under the terms of a commercial license
21
- # if purchased from OpenC3, Inc.
22
-
23
- require 'openc3/microservices/microservice'
24
- require 'openc3/models/reaction_model'
25
- require 'openc3/models/trigger_model'
26
- require 'openc3/topics/autonomic_topic'
27
- require 'openc3/utilities/authentication'
28
-
29
- require 'openc3/script'
30
-
31
- module OpenC3
32
- # This should remain a thread safe implementation. This is the in memory
33
- # cache that should mirror the database. This will update two hash
34
- # variables and will track triggers to lookup what triggers link to what
35
- # reactions.
36
- class ReactionBase
37
- attr_reader :reactions
38
-
39
- def initialize(scope:)
40
- @scope = scope
41
- @reactions_mutex = Mutex.new
42
- @reactions = Hash.new
43
- @lookup_mutex = Mutex.new
44
- @lookup = Hash.new
45
- end
46
-
47
- # RETURNS an Array of actively snoozed reactions
48
- def get_snoozed
49
- data = nil
50
- @reactions_mutex.synchronize do
51
- data = Marshal.load( Marshal.dump(@reactions) )
52
- end
53
- ret = Array.new
54
- return ret unless data
55
- data.each do |_name, r_hash|
56
- data = Marshal.load( Marshal.dump(r_hash) )
57
- reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
58
- ret << reaction if reaction.enabled && reaction.snoozed_until
59
- end
60
- return ret
61
- end
62
-
63
- # RETURNS an Array of actively NOT snoozed reactions
64
- def get_reactions(trigger_name:)
65
- array_value = nil
66
- @lookup_mutex.synchronize do
67
- array_value = Marshal.load( Marshal.dump(@lookup[trigger_name]) )
68
- end
69
- ret = Array.new
70
- return ret unless array_value
71
- array_value.each do |name|
72
- @reactions_mutex.synchronize do
73
- data = Marshal.load( Marshal.dump(@reactions[name]) )
74
- reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
75
- ret << reaction if reaction.enabled && reaction.snoozed_until.nil?
76
- end
77
- end
78
- return ret
79
- end
80
-
81
- # Update the memory database with a HASH of reactions from the external database
82
- def setup(reactions:)
83
- @reactions_mutex.synchronize do
84
- @reactions = Marshal.load( Marshal.dump(reactions) )
85
- end
86
- @lookup_mutex.synchronize do
87
- @lookup = Hash.new
88
- reactions.each do |reaction_name, reaction|
89
- reaction['triggers'].each do |trigger|
90
- trigger_name = trigger['name']
91
- if @lookup[trigger_name].nil?
92
- @lookup[trigger_name] = [reaction_name]
93
- else
94
- @lookup[trigger_name] << reaction_name
95
- end
96
- end
97
- end
98
- end
99
- end
100
-
101
- # Pulls the latest reaction name from the in memory database to see
102
- # if the reaction should be put to sleep.
103
- def sleep(name:)
104
- @reactions_mutex.synchronize do
105
- data = Marshal.load( Marshal.dump(@reactions[name]) )
106
- return unless data
107
- reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
108
- if reaction.snoozed_until.nil? || Time.now.to_i >= reaction.snoozed_until
109
- reaction.sleep()
110
- end
111
- @reactions[name] = reaction.as_json(:allow_nan => true)
112
- end
113
- end
114
-
115
- # Pulls the latest reaction name from the in memory database to see
116
- # if the reaction should be awaken.
117
- def wake(name:)
118
- @reactions_mutex.synchronize do
119
- data = Marshal.load( Marshal.dump(@reactions[name]) )
120
- return unless data
121
- reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
122
- reaction.awaken()
123
- @reactions[name] = reaction.as_json(:allow_nan => true)
124
- end
125
- end
126
-
127
- # Add a reaction to the in memory database
128
- def add(reaction:)
129
- reaction_name = reaction['name']
130
- @reactions_mutex.synchronize do
131
- @reactions[reaction_name] = reaction
132
- end
133
- reaction['triggers'].each do |trigger|
134
- trigger_name = trigger['name']
135
- @lookup_mutex.synchronize do
136
- if @lookup[trigger_name].nil?
137
- @lookup[trigger_name] = [reaction_name]
138
- else
139
- @lookup[trigger_name] << reaction_name
140
- end
141
- end
142
- end
143
- end
144
-
145
- # Updates a reaction to the in memory database. This current does not
146
- # update the lookup Hash for the triggers.
147
- def update(reaction:)
148
- @reactions_mutex.synchronize do
149
- model = ReactionModel.from_json(reaction, name: reaction['name'], scope: reaction['scope'])
150
- model.update()
151
- @reactions[reaction['name']] = model.as_json(:allow_nan => true)
152
- end
153
- end
154
-
155
- # Removes a reaction to the in memory database.
156
- def remove(reaction:)
157
- @reactions_mutex.synchronize do
158
- @reactions.delete(reaction['name'])
159
- ReactionModel.delete(name: reaction['name'], scope: reaction['scope'])
160
- end
161
- reaction['triggers'].each do |trigger|
162
- trigger_name = trigger['name']
163
- @lookup_mutex.synchronize do
164
- @lookup[trigger_name].delete(reaction['name'])
165
- end
166
- end
167
- end
168
- end
169
-
170
- # This should remain a thread safe implementation.
171
- class QueueBase
172
- attr_reader :queue
173
-
174
- def initialize(scope:)
175
- @queue = Queue.new
176
- end
177
-
178
- def enqueue(kind:, data:)
179
- @queue << [kind, data]
180
- end
181
- end
182
-
183
- # This should remain a thread safe implementation.
184
- class SnoozeBase
185
- def initialize(scope:)
186
- # store the round robin watch
187
- @watch_mutex = Mutex.new
188
- @watch_size = 25
189
- @watch_queue = Array.new(@watch_size)
190
- @watch_index = 0
191
- end
192
-
193
- def not_queued?(reaction:)
194
- key = "#{reaction.name}__#{reaction.snoozed_until}"
195
- @watch_mutex.synchronize do
196
- return false if @watch_queue.index(key)
197
- @watch_queue[@watch_index] = key
198
- @watch_index = @watch_index + 1 >= @watch_size ? 0 : @watch_index + 1
199
- return true
200
- end
201
- end
202
- end
203
-
204
- # Shared between the monitor thread and the manager thread to
205
- # share the resources.
206
- class ReactionShare
207
- attr_reader :reaction_base, :queue_base, :snooze_base
208
-
209
- def initialize(scope:)
210
- @reaction_base = ReactionBase.new(scope: scope)
211
- @queue_base = QueueBase.new(scope: scope)
212
- @snooze_base = SnoozeBase.new(scope: scope)
213
- end
214
- end
215
-
216
- # The Reaction worker is a very simple thread pool worker. Once the manager
217
- # queues a trigger to evaluate against the reactions. The worker will check
218
- # the reactions to see if it needs to fire any reactions.
219
- class ReactionWorker
220
- attr_reader :name, :scope, :share
221
-
222
- def initialize(name:, logger:, scope:, share:, ident:)
223
- @name = name
224
- @logger = logger
225
- @scope = scope
226
- @share = share
227
- @ident = ident
228
- end
229
-
230
- def get_token(username)
231
- if ENV['OPENC3_API_CLIENT'].nil?
232
- ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
233
- return OpenC3Authentication.new().token
234
- else
235
- # Check for offline access token
236
- model = nil
237
- model = OpenC3::OfflineAccessModel.get_model(name: username, scope: @scope) if username and username != ''
238
- if model and model.offline_access_token
239
- auth = OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
240
- return auth.get_token_from_refresh_token(model.offline_access_token)
241
- else
242
- return nil
243
- end
244
- end
245
- end
246
-
247
- def reaction(data:)
248
- return ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
249
- end
250
-
251
- def run
252
- @logger.info "ReactionWorker-#{@ident} running"
253
- loop do
254
- begin
255
- kind, data = @share.queue_base.queue.pop
256
- break if kind.nil? || data.nil?
257
- case kind
258
- when 'reaction'
259
- run_reaction(reaction: reaction(data: data))
260
- when 'trigger'
261
- process_true_trigger(data: data)
262
- end
263
- rescue StandardError => e
264
- @logger.error "ReactionWorker-#{@ident} failed to evaluate kind: #{kind} data: #{data}\n#{e.formatted}"
265
- end
266
- end
267
- @logger.info "ReactionWorker-#{@ident} exiting"
268
- end
269
-
270
- def process_true_trigger(data:)
271
- @share.reaction_base.get_reactions(trigger_name: data['name']).each do |reaction|
272
- run_reaction(reaction: reaction)
273
- end
274
- end
275
-
276
- def run_reaction(reaction:)
277
- reaction.actions.each do |action|
278
- run_action(reaction: reaction, action: action)
279
- end
280
- @share.reaction_base.sleep(name: reaction.name)
281
- end
282
-
283
- def run_action(reaction:, action:)
284
- reaction.updated_at = Time.now.to_nsec_from_epoch
285
- reaction_json = reaction.as_json(:allow_nan => true)
286
- # Let the frontend know which action is being run
287
- # because we can combine commands and scripts with notifications
288
- reaction_json['action'] = action['type']
289
- notification = {
290
- 'kind' => 'run',
291
- 'type' => 'reaction',
292
- 'data' => JSON.generate(reaction_json),
293
- }
294
- AutonomicTopic.write_notification(notification, scope: @scope)
295
-
296
- case action['type']
297
- when 'notify'
298
- run_notify(reaction: reaction, action: action)
299
- when 'command'
300
- run_command(reaction: reaction, action: action)
301
- when 'script'
302
- run_script(reaction: reaction, action: action)
303
- end
304
- end
305
-
306
- def run_notify(reaction:, action:)
307
- message = "ReactionWorker-#{@ident} #{reaction.name} notify action complete, body: #{action['value']}"
308
- url = "/tools/autonomic/reactions"
309
- case action['severity'].to_s.upcase
310
- when 'FATAL'
311
- @logger.fatal(message, url: url, type: Logger::ALERT)
312
- when 'ERROR', 'CRITICAL'
313
- @logger.error(message, url: url, type: Logger::ALERT)
314
- when 'WARN', 'CAUTION', 'SERIOUS'
315
- @logger.warn(message, url: url, type: Logger::NOTIFICATION)
316
- when 'INFO', 'NORMAL', 'STANDBY', 'OFF'
317
- @logger.info(message, url: url, type: Logger::NOTIFICATION)
318
- when 'DEBUG'
319
- level = @logger.level
320
- begin
321
- @logger.level = Logger::DEBUG
322
- @logger.debug(message, url: url, type: Logger::NOTIFICATION)
323
- ensure
324
- @logger.level = level
325
- end
326
- else
327
- raise "Unknown severity: #{action['severity']}"
328
- end
329
- end
330
-
331
- def run_command(reaction:, action:)
332
- begin
333
- username = reaction.username
334
- token = get_token(username)
335
- raise "No token available for username: #{username}" unless token
336
- cmd_no_hazardous_check(action['value'], scope: @scope, token: token)
337
- @logger.info "ReactionWorker-#{@ident} #{reaction.name} command action complete, command: #{action['value']}"
338
- rescue StandardError => e
339
- @logger.error "ReactionWorker-#{@ident} #{reaction.name} command action failed, #{action}\n#{e.message}"
340
- end
341
- end
342
-
343
- def run_script(reaction:, action:)
344
- begin
345
- username = reaction.username
346
- token = get_token(username)
347
- raise "No token available for username: #{username}" unless token
348
- request = Net::HTTP::Post.new(
349
- "/script-api/scripts/#{action['value']}/run?scope=#{@scope}",
350
- 'Content-Type' => 'application/json',
351
- 'Authorization' => token
352
- )
353
- request.body = JSON.generate({
354
- 'scope' => @scope,
355
- 'environment' => action['environment'],
356
- 'reaction' => reaction.name,
357
- 'id' => Time.now.to_i
358
- })
359
- hostname = ENV['OPENC3_SCRIPT_HOSTNAME'] || 'openc3-cosmos-script-runner-api'
360
- response = Net::HTTP.new(hostname, 2902).request(request)
361
- raise "failed to call #{hostname}, for script: #{action['value']}, response code: #{response.code}" if response.code != '200'
362
-
363
- @logger.info "ReactionWorker-#{@ident} #{reaction.name} script action complete, #{action['value']} => #{response.body}"
364
- rescue StandardError => e
365
- @logger.error "ReactionWorker-#{@ident} #{reaction.name} script action failed, #{action}\n#{e.message}"
366
- end
367
- end
368
- end
369
-
370
- # The reaction snooze manager starts a thread pool and keeps track of when a
371
- # reaction is activated and to evaluate triggers when the snooze is complete.
372
- class ReactionSnoozeManager
373
- attr_reader :name, :scope, :share, :thread_pool
374
-
375
- def initialize(name:, logger:, scope:, share:)
376
- @name = name
377
- @logger = logger
378
- @scope = scope
379
- @share = share
380
- @worker_count = 3
381
- @thread_pool = nil
382
- @cancel_thread = false
383
- end
384
-
385
- def generate_thread_pool()
386
- thread_pool = []
387
- @worker_count.times do |i|
388
- worker = ReactionWorker.new(name: @name, logger: @logger, scope: @scope, share: @share, ident: i)
389
- thread_pool << Thread.new { worker.run }
390
- end
391
- return thread_pool
392
- end
393
-
394
- def run
395
- @logger.info "ReactionSnoozeManager running"
396
- @thread_pool = generate_thread_pool()
397
- loop do
398
- begin
399
- current_time = Time.now.to_i
400
- manage_snoozed_reactions(current_time: current_time)
401
- rescue StandardError => e
402
- @logger.error "ReactionSnoozeManager failed to snooze reactions.\n#{e.formatted}"
403
- end
404
- break if @cancel_thread
405
- sleep(1)
406
- break if @cancel_thread
407
- end
408
- @logger.info "ReactionSnoozeManager exiting"
409
- end
410
-
411
- def active_triggers(reaction:)
412
- reaction.triggers.each do |trigger|
413
- t = TriggerModel.get(name: trigger['name'], group: trigger['group'], scope: @scope)
414
- return true if t && t.state
415
- end
416
- return false
417
- end
418
-
419
- def manage_snoozed_reactions(current_time:)
420
- @share.reaction_base.get_snoozed.each do |reaction|
421
- time_difference = reaction.snoozed_until - current_time
422
- if time_difference <= 0 && @share.snooze_base.not_queued?(reaction: reaction)
423
- # LEVEL triggers mean we run if the trigger is active
424
- if reaction.triggerLevel == 'LEVEL' and active_triggers(reaction: reaction)
425
- @share.queue_base.enqueue(kind: 'reaction', data: reaction.as_json(:allow_nan => true))
426
- else
427
- @share.reaction_base.wake(name: reaction.name)
428
- end
429
- end
430
- end
431
- end
432
-
433
- def shutdown
434
- @cancel_thread = true
435
- @worker_count.times do |_i|
436
- @share.queue_base.enqueue(kind: nil, data: nil)
437
- end
438
- end
439
- end
440
-
441
- # The reaction microservice starts a manager then gets the
442
- # reactions and triggers from redis. It then monitors the
443
- # AutonomicTopic for changes.
444
- class ReactionMicroservice < Microservice
445
- attr_reader :name, :scope, :share, :manager, :manager_thread
446
- TOPIC_LOOKUP = {
447
- 'group' => {
448
- 'created' => :no_op,
449
- 'updated' => :no_op,
450
- 'deleted' => :no_op,
451
- },
452
- 'trigger' => {
453
- 'error' => :no_op,
454
- 'created' => :no_op,
455
- 'updated' => :no_op,
456
- 'deleted' => :no_op,
457
- 'enabled' => :no_op,
458
- 'disabled' => :no_op,
459
- 'true' => :trigger_true_event,
460
- 'false' => :no_op,
461
- },
462
- 'reaction' => {
463
- 'run' => :no_op,
464
- 'deployed' => :no_op,
465
- 'undeployed' => :no_op,
466
- 'created' => :reaction_created_event,
467
- 'updated' => :reaction_updated_event,
468
- 'deleted' => :reaction_deleted_event,
469
- 'enabled' => :reaction_enabled_event,
470
- 'disabled' => :reaction_disabled_event,
471
- 'snoozed' => :no_op,
472
- 'awakened' => :no_op,
473
- 'executed' => :reaction_execute_event,
474
- }
475
- }
476
-
477
- def initialize(*args)
478
- # The name is passed in via the reaction_model as "#{scope}__OPENC3__REACTION"
479
- super(*args)
480
- @share = ReactionShare.new(scope: @scope)
481
- @manager = ReactionSnoozeManager.new(name: @name, logger: @logger, scope: @scope, share: @share)
482
- @manager_thread = nil
483
- @read_topic = true
484
- end
485
-
486
- def run
487
- @logger.info "ReactionMicroservice running"
488
- # Let the frontend know that the microservice has been deployed and is running
489
- notification = {
490
- 'kind' => 'deployed',
491
- 'type' => 'reaction',
492
- # name and updated_at fields are required for Event formatting
493
- 'data' => JSON.generate({
494
- 'name' => @name,
495
- 'updated_at' => Time.now.to_nsec_from_epoch,
496
- }),
497
- }
498
- AutonomicTopic.write_notification(notification, scope: @scope)
499
-
500
- @manager_thread = Thread.new { @manager.run }
501
- loop do
502
- reactions = ReactionModel.all(scope: @scope)
503
- @share.reaction_base.setup(reactions: reactions)
504
- break if @cancel_thread
505
- block_for_updates()
506
- break if @cancel_thread
507
- end
508
- @logger.info "ReactionMicroservice exiting"
509
- end
510
-
511
- def block_for_updates
512
- @read_topic = true
513
- while @read_topic && !@cancel_thread
514
- begin
515
- AutonomicTopic.read_topics(@topics) do |_topic, _msg_id, msg_hash, _redis|
516
- @logger.debug "ReactionMicroservice block_for_updates: #{msg_hash.to_s}"
517
- public_send(TOPIC_LOOKUP[msg_hash['type']][msg_hash['kind']], msg_hash)
518
- end
519
- rescue StandardError => e
520
- @logger.error "ReactionMicroservice failed to read topics #{@topics}\n#{e.formatted}"
521
- end
522
- end
523
- end
524
-
525
- def no_op(data)
526
- @logger.debug "ReactionMicroservice web socket event: #{data}"
527
- end
528
-
529
- def reaction_updated_event(msg_hash)
530
- @logger.debug "ReactionMicroservice reaction updated msg_hash: #{msg_hash}"
531
- reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
532
- @share.reaction_base.update(reaction: reaction)
533
- @read_topic = false
534
- end
535
-
536
- def trigger_true_event(msg_hash)
537
- @logger.debug "ReactionMicroservice trigger true msg_hash: #{msg_hash}"
538
- @share.queue_base.enqueue(kind: 'trigger', data: JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true))
539
- end
540
-
541
- # Add the reaction to the shared data.
542
- def reaction_created_event(msg_hash)
543
- @logger.debug "ReactionMicroservice reaction created msg_hash: #{msg_hash}"
544
- reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
545
- @share.reaction_base.add(reaction: reaction)
546
-
547
- # If the reaction triggerLevel is LEVEL we have to check its triggers
548
- # on add because if the trigger is active it should run
549
- if reaction['triggerLevel'] == 'LEVEL'
550
- reaction['triggers'].each do |trigger_hash|
551
- trigger = TriggerModel.get(name: trigger_hash['name'], group: trigger_hash['group'], scope: reaction['scope'])
552
- if trigger && trigger.state
553
- @logger.info "ReactionMicroservice reaction #{reaction['name']} created. Since triggerLevel is 'LEVEL' it was run due to #{trigger.name}."
554
- @share.queue_base.enqueue(kind: 'reaction', data: reaction)
555
- end
556
- end
557
- end
558
- end
559
-
560
- # Update the reaction to the shared data.
561
- def reaction_enabled_event(msg_hash)
562
- @logger.debug "ReactionMicroservice reaction enabled msg_hash: #{msg_hash}"
563
- reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
564
- @share.reaction_base.update(reaction: reaction)
565
-
566
- # If the reaction triggerLevel is LEVEL we have to check its triggers
567
- # on add because if the trigger is active it should run
568
- if reaction['triggerLevel'] == 'LEVEL'
569
- reaction['triggers'].each do |trigger_hash|
570
- trigger = TriggerModel.get(name: trigger_hash['name'], group: trigger_hash['group'], scope: reaction['scope'])
571
- if trigger && trigger.state
572
- @logger.info "ReactionMicroservice reaction #{reaction['name']} enabled. Since triggerLevel is 'LEVEL' it was run due to #{trigger.name}."
573
- @share.queue_base.enqueue(kind: 'reaction', data: reaction)
574
- end
575
- end
576
- end
577
- end
578
-
579
- # Update the reaction to the shared data.
580
- def reaction_disabled_event(msg_hash)
581
- @logger.debug "ReactionMicroservice reaction disabled msg_hash: #{msg_hash}"
582
- @share.reaction_base.update(reaction: JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true))
583
- end
584
-
585
- # Add the reaction to the shared data.
586
- def reaction_execute_event(msg_hash)
587
- @logger.debug "ReactionMicroservice reaction execute msg_hash: #{msg_hash}"
588
- reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
589
- @share.reaction_base.update(reaction: reaction)
590
- @share.queue_base.enqueue(kind: 'reaction', data: reaction)
591
- end
592
-
593
- # Remove the reaction from the shared data
594
- def reaction_deleted_event(msg_hash)
595
- @logger.debug "ReactionMicroservice reaction deleted msg_hash: #{msg_hash}"
596
- @share.reaction_base.remove(reaction: JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true))
597
- end
598
-
599
- def shutdown
600
- @read_topic = false
601
- @manager.shutdown()
602
- super
603
- end
604
- end
605
- end
606
-
607
- OpenC3::ReactionMicroservice.run if __FILE__ == $0