openc3 6.1.0 → 6.2.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/data/config/microservice.yaml +5 -0
  3. data/data/config/target.yaml +1 -1
  4. data/data/config/widgets.yaml +8 -4
  5. data/lib/openc3/api/cmd_api.rb +2 -2
  6. data/lib/openc3/api/settings_api.rb +3 -1
  7. data/lib/openc3/conversions/generic_conversion.rb +2 -2
  8. data/lib/openc3/interfaces/file_interface.rb +3 -4
  9. data/lib/openc3/interfaces/interface.rb +18 -3
  10. data/lib/openc3/interfaces/stream_interface.rb +2 -2
  11. data/lib/openc3/microservices/cleanup_microservice.rb +42 -35
  12. data/lib/openc3/microservices/decom_microservice.rb +75 -13
  13. data/lib/openc3/microservices/interface_microservice.rb +14 -3
  14. data/lib/openc3/microservices/log_microservice.rb +5 -1
  15. data/lib/openc3/microservices/microservice.rb +22 -15
  16. data/lib/openc3/microservices/multi_microservice.rb +25 -29
  17. data/lib/openc3/microservices/periodic_microservice.rb +8 -2
  18. data/lib/openc3/microservices/reducer_microservice.rb +5 -1
  19. data/lib/openc3/microservices/router_microservice.rb +5 -1
  20. data/lib/openc3/microservices/scope_cleanup_microservice.rb +12 -9
  21. data/lib/openc3/microservices/text_log_microservice.rb +5 -1
  22. data/lib/openc3/models/microservice_model.rb +8 -0
  23. data/lib/openc3/models/news_model.rb +6 -2
  24. data/lib/openc3/models/plugin_model.rb +2 -2
  25. data/lib/openc3/models/scope_model.rb +7 -1
  26. data/lib/openc3/models/target_model.rb +2 -2
  27. data/lib/openc3/operators/microservice_operator.rb +93 -47
  28. data/lib/openc3/operators/operator.rb +1 -1
  29. data/lib/openc3/packets/parsers/packet_item_parser.rb +28 -21
  30. data/lib/openc3/packets/structure.rb +19 -2
  31. data/lib/openc3/packets/structure_item.rb +1 -0
  32. data/lib/openc3/topics/decom_interface_topic.rb +1 -2
  33. data/lib/openc3/topics/interface_topic.rb +0 -2
  34. data/lib/openc3/topics/router_topic.rb +0 -2
  35. data/lib/openc3/utilities/local_mode.rb +1 -1
  36. data/lib/openc3/utilities/thread_manager.rb +83 -0
  37. data/lib/openc3/version.rb +5 -5
  38. data/templates/tool_angular/package.json +2 -2
  39. data/templates/tool_react/package.json +1 -1
  40. data/templates/tool_svelte/package.json +1 -1
  41. data/templates/tool_vue/package.json +3 -3
  42. data/templates/widget/package.json +2 -2
  43. metadata +19 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be3485e674a1fe4a236e3a71b57aff4d7d7398a0638a21d8069994739a53edab
4
- data.tar.gz: 911ee7331cc9100dc1dfbb7abe3e7f38baaa5b201be2974d882f0ef01701e9da
3
+ metadata.gz: 7afa541be45293ddc3e884af4bc3a3bdff2fb910a8b3fb1fba16f4ba5c825741
4
+ data.tar.gz: dd6e9ad3e292dfcaaa8c6b1db9807e398f46d162f8b382cca176589be39bbdf3
5
5
  SHA512:
6
- metadata.gz: eafafaac4ae90e0bb594446fefdfc7b9e6eb96be4b08c5798d827d60a2377fad9252bca9d2e4495b8d4073283aa957d89fab0214d998bd9c4bbbb5d9174000cc
7
- data.tar.gz: ea90586ba1347a74964c3cbb181810463caf507bb22c88bed8253b2fc9164b7f1648300d35163aba95f9e96f29f31ccb6f4406063dfefa7d33fd8057acddc416
6
+ metadata.gz: fb2b9b43fd05e466b804648ff255a17687f7620728d4bc646817ab04c36ecb87a2b58a8e5094d8e0488186c8142fc877f4c80ae2335b3dad0173b01bb74a02a7
7
+ data.tar.gz: 5cd84c02078c957ea4dc1df6c305f60f0f240fce674d4328114b855f70393b4c27fa6cb710aa4375449421b6fe4a0100ee925fefecf8691fbe71af37a1d85245
@@ -173,3 +173,8 @@ MICROSERVICE:
173
173
  values: \d+
174
174
  example: |
175
175
  SHARD 0
176
+ STOPPED:
177
+ summary: Initially creates the microservice in a stopped state (not enabled)
178
+ since: 6.2.0
179
+ example: |
180
+ STOPPED
@@ -154,7 +154,7 @@ TARGET:
154
154
  parameters:
155
155
  - name: Time
156
156
  required: true
157
- description: Number of seconds between runs of the cleanup process (default = 900 = 15 minutes)
157
+ description: Number of seconds between runs of the cleanup process (default = 600 = 10 minutes)
158
158
  values: \d+
159
159
  REDUCER_DISABLE:
160
160
  summary: Disables the data reduction microservice for the target
@@ -850,13 +850,17 @@ Telemetry Widgets:
850
850
  required: false
851
851
  description: Radius of the circle (default is 10)
852
852
  values: .*
853
- - name: Full Item Name
853
+ - name: Item Name Display
854
854
  required: false
855
- description: Show the full item name (default is false)
855
+ description: Show the full item name, e.g. TGT PKT ITEM (true), no item name (nil or none) or just the item name (false). Default is false.
856
856
  values: .*
857
857
  example: |
858
- LIMITSCOLOR INST HEALTH_STATUS TEMP1 CONVERTED 30 TRUE
859
- LIMITSCOLOR INST HEALTH_STATUS TEMP1
858
+ HORIZONTAL
859
+ LIMITSCOLOR INST HEALTH_STATUS TEMP1 CONVERTED 10 NIL # No label
860
+ LABEL '1st Temp'
861
+ END
862
+ LIMITSCOLOR INST HEALTH_STATUS TEMP2 # Default is label with just item name
863
+ LIMITSCOLOR INST HEALTH_STATUS TEMP3 CONVERTED 20 TRUE # Full TGT/PKT/ITEM label
860
864
  VALUELIMITSBAR:
861
865
  summary: Displays an item VALUE followed by LIMITSBAR
862
866
  parameters:
@@ -108,7 +108,7 @@ module OpenC3
108
108
  # Build a command binary
109
109
  #
110
110
  # @since 5.8.0
111
- def build_cmd(*args, range_check: true, raw: false, manual: false, scope: $openc3_scope, token: $openc3_token, **kwargs)
111
+ def build_cmd(*args, range_check: true, raw: false, manual: false, timeout: 5, scope: $openc3_scope, token: $openc3_token, **kwargs)
112
112
  extract_string_kwargs_to_args(args, kwargs)
113
113
  case args.length
114
114
  when 1
@@ -129,7 +129,7 @@ module OpenC3
129
129
  cmd_name = cmd_name.upcase
130
130
  cmd_params = cmd_params.transform_keys(&:upcase)
131
131
  authorize(permission: 'cmd_info', target_name: target_name, manual: manual, scope: scope, token: token)
132
- DecomInterfaceTopic.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, scope: scope)
132
+ DecomInterfaceTopic.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, timeout: timeout, scope: scope)
133
133
  end
134
134
  # build_command is DEPRECATED
135
135
  alias build_command build_cmd
@@ -91,7 +91,7 @@ module OpenC3
91
91
  if response.success?
92
92
  NewsModel.set(response.body)
93
93
  else
94
- NewsModel.news_error(response)
94
+ NewsModel.news_error("Error contacting OpenC3 news feed (status: #{response.status})")
95
95
  end
96
96
 
97
97
  # Test code to update the news feed with a dummy message
@@ -100,6 +100,8 @@ module OpenC3
100
100
  # json.unshift( { date: Time.now.utc.iso8601, title: "News at #{Time.now}", body: "The news feed has been updated at #{Time.now}." })
101
101
  # json.pop if json.length > 5
102
102
  # NewsModel.set(json.to_json)
103
+ rescue Exception => e
104
+ NewsModel.news_error("Error contacting OpenC3 news feed. #{e.message})")
103
105
  end
104
106
  end
105
107
  end
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -55,7 +55,7 @@ module OpenC3
55
55
  # (see OpenC3::Conversion#call)
56
56
  def call(value, packet, buffer)
57
57
  myself = packet # For backwards compatibility
58
- if true or myself # Remove unused variable warning for myself
58
+ if myself # Remove unused variable warning for myself
59
59
  return eval(@code_to_eval)
60
60
  end
61
61
  end
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2024 OpenC3, Inc.
3
+ # Copyright 2025 OpenC3, Inc.
4
4
  # All Rights Reserved.
5
5
  #
6
6
  # This program is free software; you can modify and/or redistribute it
@@ -72,15 +72,14 @@ module OpenC3
72
72
  end
73
73
 
74
74
  def connect
75
- super()
75
+ super() # Reset the protocols
76
76
 
77
77
  if @telemetry_read_folder
78
78
  @listener = Listen.to(@telemetry_read_folder, force_polling: @polling) do |modified, added, removed|
79
79
  @queue << added if added
80
80
  end
81
- @listener.start # starts a listener thread--does not block
81
+ @listener.start # starts a listener thread - does not block
82
82
  end
83
-
84
83
  @connected = true
85
84
  end
86
85
 
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -223,6 +223,21 @@ module OpenC3
223
223
  end
224
224
  end
225
225
 
226
+ # Called immediately after the interface is connected.
227
+ # By default this method will run any commands specified by the CONNECT_CMD option
228
+ def post_connect
229
+ connect_cmds = @options['CONNECT_CMD']
230
+ if connect_cmds
231
+ connect_cmds.each do |log_dont_log, cmd_string|
232
+ if log_dont_log.upcase == 'DONT_LOG'
233
+ cmd(cmd_string, log_message: false)
234
+ else
235
+ cmd(cmd_string)
236
+ end
237
+ end
238
+ end
239
+ end
240
+
226
241
  # Indicates if the interface is connected to its target(s) or not. Must be
227
242
  # implemented by a subclass.
228
243
  def connected?
@@ -499,9 +514,9 @@ module OpenC3
499
514
  def set_option(option_name, option_values)
500
515
  option_name_upcase = option_name.upcase
501
516
 
502
- # PERIODIC_CMD is special because there could be more than 1 periodic command
517
+ # CONNECT_CMD and PERIODIC_CMD are special because there could be more than 1
503
518
  # so we store them in an array for processing during connect()
504
- if option_name_upcase == 'PERIODIC_CMD'
519
+ if option_name_upcase == 'PERIODIC_CMD' or option_name_upcase == 'CONNECT_CMD'
505
520
  # OPTION PERIODIC_CMD LOG/DONT_LOG 1.0 "INST COLLECT with TYPE NORMAL"
506
521
  @options[option_name_upcase] ||= []
507
522
  @options[option_name_upcase] << option_values.clone
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -40,7 +40,7 @@ module OpenC3
40
40
  end
41
41
 
42
42
  def connect
43
- super()
43
+ super() # Reset the protocols
44
44
  @stream.connect if @stream
45
45
  end
46
46
 
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -35,44 +35,33 @@ module OpenC3
35
35
  @sleeper = Sleeper.new
36
36
  end
37
37
 
38
- def cleanup(areas, cleanup_poll_time)
39
- bucket = Bucket.getClient()
40
- while true
41
- break if @cancel_thread
42
-
43
- @state = 'GETTING_OBJECTS'
44
- start_time = Time.now
45
- areas.each do |prefix, retain_time|
46
- next unless retain_time
47
- time = start_time - retain_time
48
- oldest_list = BucketUtilities.files_between_time(ENV['OPENC3_LOGS_BUCKET'], prefix, nil, time)
49
- if oldest_list.length > 0
50
- @state = 'DELETING_OBJECTS'
51
- oldest_list.each_slice(1000) do |slice|
52
- # The delete_objects function utilizes an MD5 hash when verifying the checksums, which is not
53
- # FIPS compliant (https://github.com/aws/aws-sdk-ruby/issues/2645).
54
- # delete_object does NOT require an MD5 hash and will work on FIPS compliant systems. It is
55
- # probably less performant, but we can instead delete each item one at a time.
56
- # bucket.delete_objects(bucket: ENV['OPENC3_LOGS_BUCKET'], keys: slice)
57
- slice.each do |item|
58
- bucket.delete_object(bucket: ENV['OPENC3_LOGS_BUCKET'], key: item)
59
- end
60
- @logger.debug("Cleanup deleted #{slice.length} log files")
61
- @delete_count += slice.length
62
- @metric.set(name: 'cleanup_delete_total', value: @delete_count, type: 'counter')
38
+ def cleanup(areas, bucket)
39
+ @state = 'GETTING_OBJECTS'
40
+ start_time = Time.now
41
+ areas.each do |prefix, retain_time|
42
+ next unless retain_time
43
+ time = start_time - retain_time
44
+ oldest_list = BucketUtilities.files_between_time(ENV['OPENC3_LOGS_BUCKET'], prefix, nil, time)
45
+ if oldest_list.length > 0
46
+ @state = 'DELETING_OBJECTS'
47
+ oldest_list.each_slice(1000) do |slice|
48
+ # The delete_objects function utilizes an MD5 hash when verifying the checksums, which is not
49
+ # FIPS compliant (https://github.com/aws/aws-sdk-ruby/issues/2645).
50
+ # delete_object does NOT require an MD5 hash and will work on FIPS compliant systems. It is
51
+ # probably less performant, but we can instead delete each item one at a time.
52
+ # bucket.delete_objects(bucket: ENV['OPENC3_LOGS_BUCKET'], keys: slice)
53
+ slice.each do |item|
54
+ bucket.delete_object(bucket: ENV['OPENC3_LOGS_BUCKET'], key: item)
63
55
  end
56
+ @logger.debug("Cleanup deleted #{slice.length} log files")
57
+ @delete_count += slice.length
58
+ @metric.set(name: 'cleanup_delete_total', value: @delete_count, type: 'counter')
64
59
  end
65
60
  end
66
-
67
- @count += 1
68
- @metric.set(name: 'cleanup_total', value: @count, type: 'counter')
69
-
70
- @state = 'SLEEPING'
71
- break if @sleeper.sleep(cleanup_poll_time)
72
61
  end
73
62
  end
74
63
 
75
- def run
64
+ def get_areas_and_poll_time
76
65
  split_name = @name.split("__")
77
66
  target_name = split_name[-1]
78
67
  target = TargetModel.get_model(name: target_name, scope: @scope)
@@ -86,8 +75,22 @@ module OpenC3
86
75
  ["#{@scope}/reduced_hour_logs/tlm/#{target_name}", target.reduced_hour_log_retain_time],
87
76
  ["#{@scope}/reduced_day_logs/tlm/#{target_name}", target.reduced_day_log_retain_time],
88
77
  ]
78
+ return areas, target.cleanup_poll_time
79
+ end
89
80
 
90
- cleanup(areas, target.cleanup_poll_time)
81
+ def run
82
+ bucket = Bucket.getClient()
83
+ while true
84
+ break if @cancel_thread
85
+ areas, poll_time = get_areas_and_poll_time()
86
+ cleanup(areas, bucket)
87
+
88
+ @count += 1
89
+ @metric.set(name: 'cleanup_total', value: @count, type: 'counter')
90
+
91
+ @state = 'SLEEPING'
92
+ break if @sleeper.sleep(poll_time)
93
+ end
91
94
  end
92
95
 
93
96
  def shutdown
@@ -97,4 +100,8 @@ module OpenC3
97
100
  end
98
101
  end
99
102
 
100
- OpenC3::CleanupMicroservice.run if __FILE__ == $0
103
+ if __FILE__ == $0
104
+ OpenC3::CleanupMicroservice.run
105
+ ThreadManager.instance.shutdown
106
+ ThreadManager.instance.join
107
+ end
@@ -14,19 +14,75 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
21
21
  # if purchased from OpenC3, Inc.
22
22
 
23
23
  require 'time'
24
+ require 'thread'
24
25
  require 'openc3/microservices/microservice'
25
26
  require 'openc3/microservices/interface_decom_common'
26
27
  require 'openc3/topics/telemetry_decom_topic'
27
28
  require 'openc3/topics/limits_event_topic'
28
29
 
29
30
  module OpenC3
31
+ class LimitsResponseThread
32
+ def initialize(microservice_name:, queue:, logger:, metric:, scope:)
33
+ @microservice_name = microservice_name
34
+ @queue = queue
35
+ @logger = logger
36
+ @metric = metric
37
+ @scope = scope
38
+ @count = 0
39
+ @error_count = 0
40
+ @metric.set(name: 'limits_response_total', value: @count, type: 'counter')
41
+ @metric.set(name: 'limits_response_error_total', value: @error_count, type: 'counter')
42
+ end
43
+
44
+ def start
45
+ @thread = Thread.new do
46
+ run()
47
+ rescue Exception => e
48
+ @logger.error "#{@microservice_name}: Limits Response thread died: #{e.formatted}"
49
+ raise e
50
+ end
51
+ ThreadManager.instance.register(@thread, stop_object: self)
52
+ end
53
+
54
+ def stop
55
+ if @thread
56
+ OpenC3.kill_thread(self, @thread)
57
+ @thread = nil
58
+ end
59
+ end
60
+
61
+ def graceful_kill
62
+ @queue << [nil, nil, nil]
63
+ end
64
+
65
+ def run
66
+ while true
67
+ packet, item, old_limits_state = @queue.pop()
68
+ break if packet.nil?
69
+
70
+ begin
71
+ item.limits.response.call(packet, item, old_limits_state)
72
+ rescue Exception => e
73
+ @error_count += 1
74
+ @metric.set(name: 'limits_response_error_total', value: @error_count, type: 'counter')
75
+ @logger.error "#{packet.target_name} #{packet.packet_name} #{item.name} Limits Response Exception!"
76
+ @logger.error "Called with old_state = #{old_limits_state}, new_state = #{item.limits.state}"
77
+ @logger.error e.filtered
78
+ end
79
+
80
+ @count += 1
81
+ @metric.set(name: 'limits_response_total', value: @count, type: 'counter')
82
+ end
83
+ end
84
+ end
85
+
30
86
  class DecomMicroservice < Microservice
31
87
  include InterfaceDecomCommon
32
88
  LIMITS_STATE_INDEX = { RED_LOW: 0, YELLOW_LOW: 1, YELLOW_HIGH: 2, RED_HIGH: 3, GREEN_LOW: 4, GREEN_HIGH: 5 }
@@ -44,9 +100,14 @@ module OpenC3
44
100
  @error_count = 0
45
101
  @metric.set(name: 'decom_total', value: @count, type: 'counter')
46
102
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
103
+ @limits_response_queue = Queue.new
104
+ @limits_response_thread = nil
47
105
  end
48
106
 
49
107
  def run
108
+ @limits_response_thread = LimitsResponseThread.new(microservice_name: @name, queue: @limits_response_queue, logger: @logger, metric: @metric, scope: @scope)
109
+ @limits_response_thread.start()
110
+
50
111
  setup_microservice_topic()
51
112
  while true
52
113
  break if @cancel_thread
@@ -81,6 +142,9 @@ module OpenC3
81
142
  @logger.error("Decom error: #{e.formatted}")
82
143
  end
83
144
  end
145
+
146
+ @limits_response_thread.stop()
147
+ @limits_response_thread = nil
84
148
  end
85
149
 
86
150
  def decom_packet(_topic, msg_id, msg_hash, _redis)
@@ -119,8 +183,9 @@ module OpenC3
119
183
  # but that is rescued separately in the limits_change_callback
120
184
  packet.check_limits(System.limits_set)
121
185
 
122
- # This is what updates the CVT
186
+ # This is what actually decommutates the packet and updates the CVT
123
187
  TelemetryDecomTopic.write_packet(packet, scope: @scope)
188
+
124
189
  diff = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start # seconds as a float
125
190
  @metric.set(name: 'decom_duration_seconds', value: diff, type: 'gauge', unit: 'seconds')
126
191
  end
@@ -179,19 +244,16 @@ module OpenC3
179
244
  LimitsEventTopic.write(event, scope: @scope)
180
245
 
181
246
  if item.limits.response
182
- begin
183
- # TODO: The limits response is user code and should be run as a separate thread / process
184
- # If this code blocks it will delay TelemetryDecomTopic.write_packet
185
- item.limits.response.call(packet, item, old_limits_state)
186
- rescue Exception => e
187
- @error = e
188
- @logger.error "#{packet.target_name} #{packet.packet_name} #{item.name} Limits Response Exception!"
189
- @logger.error "Called with old_state = #{old_limits_state}, new_state = #{item.limits.state}"
190
- @logger.error e.filtered
191
- end
247
+ copied_packet = packet.deep_copy
248
+ copied_item = packet.items[item.name]
249
+ @limits_response_queue << [copied_packet, copied_item, old_limits_state]
192
250
  end
193
251
  end
194
252
  end
195
253
  end
196
254
 
197
- OpenC3::DecomMicroservice.run if __FILE__ == $0
255
+ if __FILE__ == $0
256
+ OpenC3::DecomMicroservice.run
257
+ ThreadManager.instance.shutdown
258
+ ThreadManager.instance.join
259
+ end
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -70,14 +70,16 @@ module OpenC3
70
70
  @logger.error "#{@interface.name}: Command handler thread died: #{e.formatted}"
71
71
  raise e
72
72
  end
73
+ ThreadManager.instance.register(@thread, stop_object: self)
73
74
  end
74
75
 
75
- def stop
76
+ def stop()
76
77
  OpenC3.kill_thread(self, @thread)
77
78
  end
78
79
 
79
80
  def graceful_kill
80
81
  InterfaceTopic.shutdown(@interface, scope: @scope)
82
+ sleep(0.001) # Allow other threads to run
81
83
  end
82
84
 
83
85
  def run
@@ -104,6 +106,7 @@ module OpenC3
104
106
  @metric.set(name: 'interface_directive_total', value: @directive_count, type: 'counter') if @metric
105
107
  if msg_hash['shutdown']
106
108
  @logger.info "#{@interface.name}: Shutdown requested"
109
+ InterfaceTopic.clear_topics(InterfaceTopic.topics(@interface, scope: @scope))
107
110
  return
108
111
  end
109
112
  if msg_hash['connect']
@@ -344,6 +347,7 @@ module OpenC3
344
347
  @logger.error "#{@router.name}: Telemetry handler thread died: #{e.formatted}"
345
348
  raise e
346
349
  end
350
+ ThreadManager.instance.register(@thread, stop_object: self)
347
351
  end
348
352
 
349
353
  def stop
@@ -352,6 +356,7 @@ module OpenC3
352
356
 
353
357
  def graceful_kill
354
358
  RouterTopic.shutdown(@router, scope: @scope)
359
+ sleep(0.001) # Allow other threads to run
355
360
  end
356
361
 
357
362
  def run
@@ -367,6 +372,7 @@ module OpenC3
367
372
 
368
373
  if msg_hash['shutdown']
369
374
  @logger.info "#{@router.name}: Shutdown requested"
375
+ RouterTopic.clear_topics(RouterTopic.topics(@router, scope: @scope))
370
376
  return
371
377
  end
372
378
  if msg_hash['connect']
@@ -726,6 +732,7 @@ module OpenC3
726
732
  @logger.info "#{@interface.name}: Connect #{@interface.connection_string}"
727
733
  begin
728
734
  @interface.connect
735
+ @interface.post_connect
729
736
  rescue Exception => e
730
737
  begin
731
738
  @interface.disconnect # Ensure disconnect is called at least once on a partial connect
@@ -808,4 +815,8 @@ module OpenC3
808
815
  end
809
816
  end
810
817
 
811
- OpenC3::InterfaceMicroservice.run if __FILE__ == $0
818
+ if __FILE__ == $0
819
+ OpenC3::InterfaceMicroservice.run
820
+ ThreadManager.instance.shutdown
821
+ ThreadManager.instance.join
822
+ end
@@ -142,4 +142,8 @@ module OpenC3
142
142
  end
143
143
  end
144
144
 
145
- OpenC3::LogMicroservice.run if __FILE__ == $0
145
+ if __FILE__ == $0
146
+ OpenC3::LogMicroservice.run
147
+ ThreadManager.instance.shutdown
148
+ ThreadManager.instance.join
149
+ end
@@ -30,6 +30,7 @@ OpenC3.require_file 'openc3/utilities/bucket'
30
30
  OpenC3.require_file 'openc3/utilities/secrets'
31
31
  OpenC3.require_file 'openc3/utilities/sleeper'
32
32
  OpenC3.require_file 'openc3/utilities/open_telemetry'
33
+ OpenC3.require_file 'openc3/utilities/thread_manager'
33
34
  OpenC3.require_file 'openc3/models/microservice_model'
34
35
  OpenC3.require_file 'openc3/models/microservice_status_model'
35
36
  OpenC3.require_file 'tmpdir'
@@ -49,22 +50,27 @@ module OpenC3
49
50
  def self.run(name = nil)
50
51
  name = ENV['OPENC3_MICROSERVICE_NAME'] unless name
51
52
  microservice = self.new(name)
52
- begin
53
- MicroserviceStatusModel.set(microservice.as_json(:allow_nan => true), scope: microservice.scope)
54
- microservice.state = 'RUNNING'
55
- microservice.run
56
- microservice.state = 'FINISHED'
57
- rescue Exception => e
58
- if SystemExit === e or SignalException === e
59
- microservice.state = 'KILLED'
60
- else
61
- microservice.error = e
62
- microservice.state = 'DIED_ERROR'
63
- Logger.fatal("Microservice #{name} dying from exception\n#{e.formatted}")
53
+ thread = Thread.new do
54
+ begin
55
+ MicroserviceStatusModel.set(microservice.as_json(:allow_nan => true), scope: microservice.scope)
56
+ microservice.state = 'RUNNING'
57
+ microservice.run
58
+ microservice.state = 'FINISHED'
59
+ rescue Exception => e
60
+ if SystemExit === e or SignalException === e
61
+ microservice.state = 'KILLED'
62
+ else
63
+ microservice.error = e
64
+ microservice.state = 'DIED_ERROR'
65
+ Logger.fatal("Microservice #{name} dying from exception\n#{e.formatted}")
66
+ end
67
+ ensure
68
+ MicroserviceStatusModel.set(microservice.as_json(:allow_nan => true), scope: microservice.scope)
64
69
  end
65
- ensure
66
- MicroserviceStatusModel.set(microservice.as_json(:allow_nan => true), scope: microservice.scope)
67
70
  end
71
+ ThreadManager.instance.register(thread, shutdown_object: microservice)
72
+ ThreadManager.instance.monitor
73
+ ThreadManager.instance.shutdown
68
74
  end
69
75
 
70
76
  def as_json(*a)
@@ -192,6 +198,7 @@ module OpenC3
192
198
  @logger.error "#{@name} status thread died: #{e.formatted}"
193
199
  raise e
194
200
  end
201
+ ThreadManager.instance.register(@microservice_status_thread)
195
202
  end
196
203
  end
197
204
 
@@ -206,7 +213,7 @@ module OpenC3
206
213
  @cancel_thread = true
207
214
  @microservice_status_sleeper.cancel if @microservice_status_sleeper
208
215
  MicroserviceStatusModel.set(as_json(:allow_nan => true), scope: @scope)
209
- FileUtils.remove_entry(@temp_dir) if File.exist?(@temp_dir)
216
+ FileUtils.remove_entry_secure(@temp_dir, true)
210
217
  @metric.shutdown
211
218
  @logger.debug("Shutting down microservice complete: #{@name}")
212
219
  @shutdown_complete = true
@@ -18,45 +18,41 @@
18
18
 
19
19
  require 'openc3/microservices/microservice'
20
20
  require 'openc3/topics/topic'
21
+ require 'openc3/utilities/thread_manager'
21
22
 
22
23
  module OpenC3
23
24
  class MultiMicroservice < Microservice
24
25
  def run
25
- @threads = []
26
26
  ARGV.each do |microservice_name|
27
27
  microservice_model = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
28
- @threads << Thread.new do
29
- cmd_line = microservice_model.cmd.join(' ')
30
- split_cmd_line = cmd_line.split(' ')
31
- filename = nil
32
- split_cmd_line.each do |item|
33
- if File.extname(item) == '.rb'
34
- filename = item
35
- break
28
+ if microservice_model.enabled
29
+ thread = Thread.new do
30
+ cmd_line = microservice_model.cmd.join(' ')
31
+ split_cmd_line = cmd_line.split(' ')
32
+ filename = nil
33
+ split_cmd_line.each do |item|
34
+ if File.extname(item) == '.rb'
35
+ filename = item
36
+ break
37
+ end
36
38
  end
39
+ raise "Could not determine class filename from '#{cmd_line}'" unless filename
40
+ OpenC3.set_working_dir(microservice_model.work_dir) do
41
+ require File.join(microservice_model.work_dir, filename)
42
+ end
43
+ klass = filename.filename_to_class_name.to_class
44
+ klass.run(microservice_model.name)
37
45
  end
38
- raise "Could not determine class filename from '#{cmd_line}'" unless filename
39
- OpenC3.set_working_dir(microservice_model.work_dir) do
40
- require File.join(microservice_model.work_dir, filename)
41
- end
42
- klass = filename.filename_to_class_name.to_class
43
- klass.run(microservice_model.name)
44
- end
45
- end
46
- @threads.each do |thread|
47
- thread.join
48
- end
49
- end
50
-
51
- def shutdown
52
- super()
53
- if @threads
54
- @threads.each do |thread|
55
- thread.join
46
+ ThreadManager.instance.register(thread)
56
47
  end
57
48
  end
49
+ ThreadManager.instance.monitor
50
+ ThreadManager.instance.shutdown
58
51
  end
59
52
  end
60
53
  end
61
-
62
- OpenC3::MultiMicroservice.run if __FILE__ == $0
54
+ if __FILE__ == $0
55
+ OpenC3::MultiMicroservice.run
56
+ ThreadManager.instance.shutdown
57
+ ThreadManager.instance.join
58
+ end