openc3 5.20.0 → 6.0.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 (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 +6 -6
  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