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
@@ -48,9 +48,11 @@ module OpenC3
48
48
  if response.success?
49
49
  NewsModel.set(response.body)
50
50
  else
51
- NewsModel.news_error(response)
51
+ NewsModel.news_error("Error contacting OpenC3 news feed (status: #{response.status})")
52
52
  end
53
53
  end
54
+ rescue Exception => e
55
+ NewsModel.news_error("Error contacting OpenC3 news feed. #{e.message})")
54
56
  end
55
57
 
56
58
  def run
@@ -87,4 +89,8 @@ module OpenC3
87
89
  end
88
90
  end
89
91
 
90
- OpenC3::PeriodicMicroservice.run if __FILE__ == $0
92
+ if __FILE__ == $0
93
+ OpenC3::PeriodicMicroservice.run
94
+ ThreadManager.instance.shutdown
95
+ ThreadManager.instance.join
96
+ end
@@ -633,4 +633,8 @@ module OpenC3
633
633
  end
634
634
  end
635
635
 
636
- OpenC3::ReducerMicroservice.run if __FILE__ == $0
636
+ if __FILE__ == $0
637
+ OpenC3::ReducerMicroservice.run
638
+ ThreadManager.instance.shutdown
639
+ ThreadManager.instance.join
640
+ end
@@ -86,4 +86,8 @@ module OpenC3
86
86
  end
87
87
  end
88
88
 
89
- OpenC3::RouterMicroservice.run if __FILE__ == $0
89
+ if __FILE__ == $0
90
+ OpenC3::RouterMicroservice.run
91
+ ThreadManager.instance.shutdown
92
+ ThreadManager.instance.join
93
+ end
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2023 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
@@ -21,22 +21,25 @@ require 'openc3/microservices/cleanup_microservice'
21
21
 
22
22
  module OpenC3
23
23
  class ScopeCleanupMicroservice < CleanupMicroservice
24
- def run
24
+ def get_areas_and_poll_time
25
25
  scope = ScopeModel.get_model(name: @scope)
26
-
27
26
  areas = [
28
- ["#{@scope}/text_logs", scope.text_log_retain_time],
29
- ["#{@scope}/tool_logs", scope.tool_log_retain_time],
27
+ ["#{@scope}/text_logs/openc3_log_messages", scope.text_log_retain_time],
28
+ ["#{@scope}/tool_logs/sr", scope.tool_log_retain_time],
30
29
  ]
31
30
 
32
31
  if @scope == 'DEFAULT'
33
- areas << ["NOSCOPE/text_logs", scope.text_log_retain_time]
34
- areas << ["NOSCOPE/tool_logs", scope.tool_log_retain_time]
32
+ areas << ["NOSCOPE/text_logs/openc3_log_messages", scope.text_log_retain_time]
33
+ areas << ["NOSCOPE/tool_logs/sr", scope.tool_log_retain_time]
35
34
  end
36
35
 
37
- cleanup(areas, scope.cleanup_poll_time)
36
+ return areas, scope.cleanup_poll_time
38
37
  end
39
38
  end
40
39
  end
41
40
 
42
- OpenC3::CleanupMicroservice.run if __FILE__ == $0
41
+ if __FILE__ == $0
42
+ OpenC3::ScopeCleanupMicroservice.run
43
+ ThreadManager.instance.shutdown
44
+ ThreadManager.instance.join
45
+ end
@@ -102,4 +102,8 @@ module OpenC3
102
102
  end
103
103
  end
104
104
 
105
- OpenC3::TextLogMicroservice.run if __FILE__ == $0
105
+ if __FILE__ == $0
106
+ OpenC3::TextLogMicroservice.run
107
+ ThreadManager.instance.shutdown
108
+ ThreadManager.instance.join
109
+ end
@@ -45,6 +45,7 @@ module OpenC3
45
45
  attr_accessor :disable_erb
46
46
  attr_accessor :ignore_changes
47
47
  attr_accessor :shard
48
+ attr_accessor :enabled
48
49
 
49
50
  # NOTE: The following three class methods are used by the ModelController
50
51
  # and are reimplemented to enable various Model class methods to work
@@ -107,6 +108,7 @@ module OpenC3
107
108
  disable_erb: nil,
108
109
  ignore_changes: nil,
109
110
  shard: 0,
111
+ enabled: true,
110
112
  scope:
111
113
  )
112
114
  parts = name.split("__")
@@ -134,6 +136,8 @@ module OpenC3
134
136
  @disable_erb = disable_erb
135
137
  @ignore_changes = ignore_changes
136
138
  @shard = shard.to_i # to_i to handle nil
139
+ @enabled = enabled
140
+ @enabled = true if @enabled.nil?
137
141
  @bucket = Bucket.getClient()
138
142
  end
139
143
 
@@ -158,6 +162,7 @@ module OpenC3
158
162
  'disable_erb' => @disable_erb,
159
163
  'ignore_changes' => @ignore_changes,
160
164
  'shard' => @shard,
165
+ 'enabled' => @enabled,
161
166
  }
162
167
  end
163
168
 
@@ -222,6 +227,9 @@ module OpenC3
222
227
  when 'SHARD'
223
228
  parser.verify_num_parameters(1, 1, "#{keyword} <Shard Number Starting from 0>")
224
229
  @shard = Integer(parameters[0])
230
+ when 'STOPPED'
231
+ parser.verify_num_parameters(0, 0, "#{keyword}")
232
+ @enabled = false
225
233
  else
226
234
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Microservice: #{keyword} #{parameters.join(" ")}")
227
235
  end
@@ -31,8 +31,12 @@ module OpenC3
31
31
  Store.get(PRIMARY_KEY)
32
32
  end
33
33
 
34
- def self.news_error(response)
35
- Store.set(PRIMARY_KEY, [{ date: Time.now.utc.iso8601, title: 'News Error', body: "Error contacting OpenC3 news feed (status: #{response.status})" }].to_json)
34
+ def self.news_error(message)
35
+ Store.set(PRIMARY_KEY, [{
36
+ date: Time.now.utc.iso8601,
37
+ title: 'News Error',
38
+ body: message
39
+ }].to_json)
36
40
  end
37
41
  end
38
42
  end
@@ -134,7 +134,7 @@ module OpenC3
134
134
  result['existing_plugin_txt_lines'] = existing_plugin_txt_lines if existing_plugin_txt_lines and not process_existing and existing_plugin_txt_lines != result['plugin_txt_lines']
135
135
  return result
136
136
  ensure
137
- FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
137
+ FileUtils.remove_entry_secure(temp_dir, true)
138
138
  tf.unlink if tf
139
139
  end
140
140
  end
@@ -287,7 +287,7 @@ module OpenC3
287
287
  plugin_model.destroy unless validate_only
288
288
  raise e
289
289
  ensure
290
- FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
290
+ FileUtils.remove_entry_secure(temp_dir, true)
291
291
  tf.unlink if tf
292
292
  end
293
293
  return plugin_model.as_json(:allow_nan => true)
@@ -95,7 +95,7 @@ module OpenC3
95
95
  text_log_cycle_size: 50_000_000,
96
96
  text_log_retain_time: nil,
97
97
  tool_log_retain_time: nil,
98
- cleanup_poll_time: 900,
98
+ cleanup_poll_time: 600,
99
99
  command_authority: false,
100
100
  critical_commanding: "OFF",
101
101
  shard: 0,
@@ -129,6 +129,12 @@ module OpenC3
129
129
  raise "Invalid scope name: #{@name}" if @name !~ /^[a-zA-Z0-9_-]+$/
130
130
  @name = @name.upcase
131
131
  @scope = @name # Ensure @scope matches @name
132
+ # Ensure the various cycle and retain times are integers
133
+ @text_log_cycle_time = @text_log_cycle_time.to_i
134
+ @text_log_cycle_size = @text_log_cycle_size.to_i
135
+ @text_log_retain_time = @text_log_retain_time.to_i if @text_log_retain_time
136
+ @tool_log_retain_time = @tool_log_retain_time.to_i if @tool_log_retain_time
137
+ @cleanup_poll_time = @cleanup_poll_time.to_i
132
138
  super(update: update, force: force, queued: queued)
133
139
 
134
140
  if ENTERPRISE
@@ -340,7 +340,7 @@ module OpenC3
340
340
  reduced_minute_log_retain_time: nil,
341
341
  reduced_hour_log_retain_time: nil,
342
342
  reduced_day_log_retain_time: nil,
343
- cleanup_poll_time: 900,
343
+ cleanup_poll_time: 600,
344
344
  needs_dependencies: false,
345
345
  target_microservices: {'REDUCER' => [[]]},
346
346
  reducer_disable: false,
@@ -619,7 +619,7 @@ module OpenC3
619
619
  ConfigTopic.write({ kind: 'created', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
620
620
  end
621
621
  ensure
622
- FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
622
+ FileUtils.remove_entry_secure(temp_dir, true)
623
623
  end
624
624
  end
625
625
 
@@ -84,6 +84,96 @@ module OpenC3
84
84
  return process_definition, work_dir, env, scope, container
85
85
  end
86
86
 
87
+ # Handle the detection of a new microservice model
88
+ def handle_new_microservice(microservice_name, microservice_config)
89
+ parent = microservice_config['parent']
90
+ enabled = microservice_config['enabled']
91
+ enabled = true if enabled.nil?
92
+ scope = microservice_name.split("__")[0]
93
+
94
+ if enabled
95
+ Logger.info("New microservice detected: #{microservice_name}", scope: scope)
96
+ if parent
97
+ # Respawn parent if it exists and isn't new
98
+ if @microservices[parent] and @previous_microservices[parent]
99
+ @changed_microservices[parent] = @microservices[parent]
100
+ end
101
+ else
102
+ # New process be spawned
103
+ @new_microservices[microservice_name] = microservice_config
104
+ end
105
+ end
106
+ end
107
+
108
+ # Handle a change detected in a microservice model
109
+ def handle_changed_microservice(microservice_name, microservice_config)
110
+ parent = microservice_config['parent']
111
+ enabled = microservice_config['enabled']
112
+ enabled = true if enabled.nil?
113
+ scope = microservice_name.split("__")[0]
114
+ previous_parent = @previous_microservices[microservice_name]['parent']
115
+ previous_enabled = @previous_microservices[microservice_name]["enabled"]
116
+ previous_enabled = true if previous_enabled.nil?
117
+
118
+ Logger.info("Changed microservice detected: #{microservice_name}\nWas: #{@previous_microservices[microservice_name]}\nIs: #{microservice_config}", scope: scope)
119
+
120
+ if parent or previous_parent
121
+ if parent == previous_parent
122
+ # Same Parent - Respawn parent
123
+ @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
124
+ elsif parent and previous_parent
125
+ # Parent changed - Respawn both parents
126
+ @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
127
+ @changed_microservices[previous_parent] = @microservices[previous_parent] if @microservices[previous_parent] and @previous_microservices[previous_parent]
128
+ elsif parent
129
+ # Moved under a parent - Respawn parent and kill standalone (if previously enabled)
130
+ @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
131
+ if previous_enabled
132
+ @removed_microservices[microservice_name] = microservice_config
133
+ end
134
+ else # previous_parent
135
+ # Moved to standalone - Respawn previous parent and make new (if enabled)
136
+ @changed_microservices[previous_parent] = @microservices[previous_parent] if @microservices[previous_parent] and @previous_microservices[previous_parent]
137
+ if enabled
138
+ @new_microservices[microservice_name] = microservice_config
139
+ end
140
+ end
141
+ else
142
+ if previous_enabled
143
+ if enabled
144
+ # Respawn regular microservice
145
+ @changed_microservices[microservice_name] = microservice_config
146
+ else
147
+ # Remove regular microservice
148
+ @removed_microservices[microservice_name] = microservice_config
149
+ end
150
+ else
151
+ # Newly enabled microservice
152
+ @new_microservices[microservice_name] = microservice_config
153
+ end
154
+ end
155
+ end
156
+
157
+ # Handle the detection of a removed microservice model
158
+ def handle_removed_microservice(microservice_name, microservice_config)
159
+ previous_parent = @previous_microservices[microservice_name]['parent']
160
+ scope = microservice_name.split("__")[0]
161
+
162
+ Logger.info("Removed microservice detected: #{microservice_name}", scope: scope)
163
+
164
+ if previous_parent
165
+ # Respawn previous parent
166
+ @changed_microservices[previous_parent] = @microservices[previous_parent] if @microservices[previous_parent] and @previous_microservices[previous_parent]
167
+ else
168
+ previous_enabled = @previous_microservices[microservice_name]["enabled"]
169
+ previous_enabled = true if previous_enabled.nil?
170
+ if previous_enabled
171
+ # Regular process to be removed
172
+ @removed_microservices[microservice_name] = microservice_config
173
+ end
174
+ end
175
+ end
176
+
87
177
  def update
88
178
  @previous_microservices = @microservices.dup
89
179
  # Get all the microservice configuration
@@ -100,65 +190,21 @@ module OpenC3
100
190
  @changed_microservices = {}
101
191
  @removed_microservices = {}
102
192
  @microservices.each do |microservice_name, microservice_config|
103
- parent = microservice_config['parent']
104
193
  if @previous_microservices[microservice_name]
105
- previous_parent = @previous_microservices[microservice_name]['parent']
106
194
  if @previous_microservices[microservice_name] != microservice_config
107
- # CHANGED
108
195
  if not microservice_config['ignore_changes']
109
- scope = microservice_name.split("__")[0]
110
- Logger.info("Changed microservice detected: #{microservice_name}\nWas: #{@previous_microservices[microservice_name]}\nIs: #{microservice_config}", scope: scope)
111
- if parent or previous_parent
112
- if parent == previous_parent
113
- # Same Parent - Respawn parent
114
- @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
115
- elsif parent and previous_parent
116
- # Parent changed - Respawn both parents
117
- @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
118
- @changed_microservices[previous_parent] = @microservices[previous_parent] if @microservices[previous_parent] and @previous_microservices[previous_parent]
119
- elsif parent
120
- # Moved under a parent - Respawn parent and kill standalone
121
- @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
122
- @removed_microservices[microservice_name] = microservice_config
123
- else # previous_parent
124
- # Moved to standalone - Respawn previous parent and make new
125
- @changed_microservices[previous_parent] = @microservices[previous_parent] if @microservices[previous_parent] and @previous_microservices[previous_parent]
126
- @new_microservices[microservice_name] = microservice_config
127
- end
128
- else
129
- # Respawn regular microservice
130
- @changed_microservices[microservice_name] = microservice_config
131
- end
196
+ handle_changed_microservice(microservice_name, microservice_config)
132
197
  end
133
198
  end
134
199
  else
135
- # NEW
136
- scope = microservice_name.split("__")[0]
137
- Logger.info("New microservice detected: #{microservice_name}", scope: scope)
138
- if parent
139
- # Respawn parent
140
- @changed_microservices[parent] = @microservices[parent] if @microservices[parent] and @previous_microservices[parent]
141
- else
142
- # New process be spawned
143
- @new_microservices[microservice_name] = microservice_config
144
- end
200
+ handle_new_microservice(microservice_name, microservice_config)
145
201
  end
146
202
  end
147
203
 
148
204
  # Detect removed microservices
149
205
  @previous_microservices.each do |microservice_name, microservice_config|
150
- previous_parent = microservice_config['parent']
151
206
  unless @microservices[microservice_name]
152
- # REMOVED
153
- scope = microservice_name.split("__")[0]
154
- Logger.info("Removed microservice detected: #{microservice_name}", scope: scope)
155
- if previous_parent
156
- # Respawn previous parent
157
- @changed_microservices[previous_parent] = @microservices[previous_parent] if @microservices[previous_parent] and @previous_microservices[previous_parent]
158
- else
159
- # Regular process to be removed
160
- @removed_microservices[microservice_name] = microservice_config
161
- end
207
+ handle_removed_microservice(microservice_name, microservice_config)
162
208
  end
163
209
  end
164
210
 
@@ -179,7 +179,7 @@ module OpenC3
179
179
  end
180
180
  @process.stop
181
181
  end
182
- FileUtils.remove_entry(@temp_dir) if @temp_dir and File.exist?(@temp_dir)
182
+ FileUtils.remove_entry_secure(@temp_dir, true)
183
183
  @process = nil
184
184
  end
185
185
 
@@ -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 2023, 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
@@ -80,7 +80,7 @@ module OpenC3
80
80
  item.range = get_range()
81
81
  item.default = get_default()
82
82
  end
83
- item.id_value = get_id_value()
83
+ item.id_value = get_id_value(item)
84
84
  item.description = get_description()
85
85
  if append?
86
86
  item = packet.append(item)
@@ -165,6 +165,18 @@ module OpenC3
165
165
  min..max
166
166
  end
167
167
 
168
+ def convert_string_value(index)
169
+ # If the default value is 0x<data> (no quotes), it is treated as
170
+ # binary data. Otherwise, the default value is considered to be a string.
171
+ if @parser.parameters[index].upcase.start_with?("0X") and
172
+ !@parser.line.include?("\"#{@parser.parameters[index]}\"") and
173
+ !@parser.line.include?("\'#{@parser.parameters[index]}\'")
174
+ return @parser.parameters[index].hex_to_byte_string
175
+ else
176
+ return @parser.parameters[index]
177
+ end
178
+ end
179
+
168
180
  def get_default
169
181
  return [] if @parser.keyword.include?('ARRAY')
170
182
 
@@ -173,15 +185,7 @@ module OpenC3
173
185
  return [] if data_type == :ARRAY
174
186
  return {} if data_type == :OBJECT
175
187
  if data_type == :STRING or data_type == :BLOCK
176
- # If the default value is 0x<data> (no quotes), it is treated as
177
- # binary data. Otherwise, the default value is considered to be a string.
178
- if @parser.parameters[index].upcase.start_with?("0X") and
179
- !@parser.line.include?("\"#{@parser.parameters[index]}\"") and
180
- !@parser.line.include?("\'#{@parser.parameters[index]}\'")
181
- return @parser.parameters[index].hex_to_byte_string
182
- else
183
- return @parser.parameters[index]
184
- end
188
+ return convert_string_value(index)
185
189
  else
186
190
  if data_type != :DERIVED
187
191
  return ConfigParser.handle_defined_constants(
@@ -193,22 +197,25 @@ module OpenC3
193
197
  end
194
198
  end
195
199
 
196
- def get_id_value
200
+ def get_id_value(item)
197
201
  return nil unless @parser.keyword.include?('ID_')
198
-
199
202
  data_type = get_data_type
200
- if @parser.keyword.include?('ITEM')
201
- index = append? ? 3 : 4
202
- else # PARAMETER
203
- index = append? ? 5 : 6
204
- # STRING and BLOCK PARAMETERS don't have min and max values
205
- index -= 2 if data_type == :STRING || data_type == :BLOCK
206
- end
207
203
  if data_type == :DERIVED
208
204
  raise @parser.error("DERIVED data type not allowed for Identifier")
209
205
  end
206
+ # For PARAMETERS the default value is the ID value
207
+ if @parser.keyword.include?("PARAMETER")
208
+ return item.default
209
+ end
210
210
 
211
- @parser.parameters[index]
211
+ index = append? ? 3 : 4
212
+ if data_type == :STRING or data_type == :BLOCK
213
+ return convert_string_value(index)
214
+ else
215
+ return ConfigParser.handle_defined_constants(
216
+ @parser.parameters[index].convert_to_value, data_type, get_bit_size()
217
+ )
218
+ end
212
219
  end
213
220
 
214
221
  def get_description
@@ -35,10 +35,10 @@ module OpenC3
35
35
 
36
36
  # @return [Hash] Items that make up the structure.
37
37
  # Hash key is the item's name in uppercase
38
- attr_reader :items
38
+ attr_accessor :items
39
39
 
40
40
  # @return [Array] Items sorted by bit_offset.
41
- attr_reader :sorted_items
41
+ attr_accessor :sorted_items
42
42
 
43
43
  # @return [Integer] Defined length in bytes (not bits) of the structure
44
44
  attr_reader :defined_length
@@ -511,11 +511,28 @@ module OpenC3
511
511
  # additional work that isn't necessary here
512
512
  structure.instance_variable_set("@buffer".freeze, @buffer.clone) if @buffer
513
513
  # Need to update reference packet in the Accessor
514
+ structure.accessor = @accessor.clone
514
515
  structure.accessor.packet = structure
515
516
  return structure
516
517
  end
517
518
  alias dup clone
518
519
 
520
+ # Clone that also deep copies items
521
+ # @return [Structure] A deep copy of the structure
522
+ def deep_copy
523
+ cloned = clone()
524
+ cloned_items = []
525
+ cloned.sorted_items.each do |item|
526
+ cloned_items << item.clone()
527
+ end
528
+ cloned.sorted_items = cloned_items
529
+ cloned.items = {}
530
+ cloned_items.each do |item|
531
+ cloned.items[item.name] = item
532
+ end
533
+ return cloned
534
+ end
535
+
519
536
  # Enable the ability to read and write item values as if they were methods
520
537
  # to the class
521
538
  def enable_method_missing
@@ -303,6 +303,7 @@ module OpenC3
303
303
  def clone
304
304
  item = super()
305
305
  item.name = self.name.clone if self.name
306
+ item.key = self.key.clone if self.key
306
307
  item
307
308
  end
308
309
  alias dup clone
@@ -20,7 +20,7 @@ require 'openc3/topics/topic'
20
20
 
21
21
  module OpenC3
22
22
  class DecomInterfaceTopic < Topic
23
- def self.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, scope:)
23
+ def self.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, timeout: 5, scope:)
24
24
  data = {}
25
25
  data['target_name'] = target_name.to_s.upcase
26
26
  data['cmd_name'] = cmd_name.to_s.upcase
@@ -34,7 +34,6 @@ module OpenC3
34
34
  Topic.update_topic_offsets([ack_topic])
35
35
  decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
36
36
  { 'build_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
37
- timeout = 5 # Arbitrary 5s timeout
38
37
  time = Time.now
39
38
  while (Time.now - time) < timeout
40
39
  Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
@@ -75,8 +75,6 @@ module OpenC3
75
75
 
76
76
  def self.shutdown(interface, scope:)
77
77
  Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface.name}", { 'shutdown' => 'true' }, '*', 100)
78
- sleep 1 # Give some time for the interface to shutdown
79
- InterfaceTopic.clear_topics(InterfaceTopic.topics(interface, scope: scope))
80
78
  end
81
79
 
82
80
  def self.interface_cmd(interface_name, cmd_name, *cmd_params, scope:)
@@ -85,8 +85,6 @@ module OpenC3
85
85
 
86
86
  def self.shutdown(router, scope:)
87
87
  Topic.write_topic("{#{scope}__CMD}ROUTER__#{router.name}", { 'shutdown' => 'true' }, '*', 100)
88
- sleep 1 # Give some time for the interface to shutdown
89
- RouterTopic.clear_topics(RouterTopic.topics(router, scope: scope))
90
88
  end
91
89
 
92
90
  def self.router_cmd(router_name, cmd_name, *cmd_params, scope:)
@@ -301,7 +301,7 @@ module OpenC3
301
301
  file.write(JSON.pretty_generate(plugin_hash, :allow_nan => true))
302
302
  end
303
303
  ensure
304
- FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
304
+ FileUtils.remove_entry_secure(temp_dir, true)
305
305
  end
306
306
  end
307
307
 
@@ -0,0 +1,83 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 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
+ module OpenC3
20
+ class ThreadManager
21
+ MONITOR_SLEEP_SECONDS = 0.25
22
+
23
+ # Variable that holds the singleton instance
24
+ @@instance = nil
25
+
26
+ # Mutex used to ensure that only one instance of is created
27
+ @@instance_mutex = Mutex.new
28
+
29
+ # Get the singleton instance of ThreadManager
30
+ def self.instance
31
+ return @@instance if @@instance
32
+
33
+ @@instance_mutex.synchronize do
34
+ return @@instance if @@instance
35
+ @@instance ||= self.new
36
+ return @@instance
37
+ end
38
+ end
39
+
40
+ def initialize
41
+ @threads = []
42
+ @shutdown_started = false
43
+ end
44
+
45
+ def register(thread, stop_object: nil, shutdown_object: nil)
46
+ @threads << [thread, stop_object, shutdown_object]
47
+ end
48
+
49
+ def monitor
50
+ while true
51
+ @threads.each do |thread, _, _|
52
+ if !thread.alive?
53
+ return
54
+ end
55
+ end
56
+ sleep(MONITOR_SLEEP_SECONDS)
57
+ end
58
+ end
59
+
60
+ def shutdown
61
+ @@instance_mutex.synchronize do
62
+ return if @shutdown_started
63
+ @shutdown_started = true
64
+ end
65
+ @threads.each do |thread, stop_object, shutdown_object|
66
+ if thread.alive?
67
+ if stop_object
68
+ stop_object.stop
69
+ end
70
+ if shutdown_object
71
+ shutdown_object.shutdown
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def join
78
+ @threads.each do |thread, _, _|
79
+ thread.join
80
+ end
81
+ end
82
+ end
83
+ end