openc3 7.0.0.pre.rc2 → 7.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +13 -4
  3. data/bin/pipinstall +6 -7
  4. data/bin/pipuninstall +3 -5
  5. data/data/config/interface_modifiers.yaml +1 -1
  6. data/data/config/item_modifiers.yaml +18 -6
  7. data/data/config/telemetry.yaml +1 -1
  8. data/data/config/widgets.yaml +10 -0
  9. data/lib/openc3/accessors/json_accessor.rb +1 -1
  10. data/lib/openc3/api/cmd_api.rb +2 -0
  11. data/lib/openc3/api/settings_api.rb +2 -0
  12. data/lib/openc3/api/tlm_api.rb +3 -3
  13. data/lib/openc3/config/config_parser.rb +4 -4
  14. data/lib/openc3/conversions/conversion.rb +3 -3
  15. data/lib/openc3/core_ext/faraday.rb +4 -0
  16. data/lib/openc3/logs/log_writer.rb +24 -6
  17. data/lib/openc3/logs/packet_log_writer.rb +1 -4
  18. data/lib/openc3/logs/stream_log_pair.rb +11 -4
  19. data/lib/openc3/logs/text_log_writer.rb +1 -4
  20. data/lib/openc3/microservices/interface_microservice.rb +8 -2
  21. data/lib/openc3/microservices/log_microservice.rb +7 -2
  22. data/lib/openc3/microservices/microservice.rb +10 -4
  23. data/lib/openc3/microservices/queue_microservice.rb +9 -2
  24. data/lib/openc3/microservices/scope_cleanup_microservice.rb +116 -1
  25. data/lib/openc3/microservices/text_log_microservice.rb +4 -1
  26. data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +1 -1
  27. data/lib/openc3/migrations/20250402000000_periodic_only_default.rb +1 -1
  28. data/lib/openc3/migrations/20260203000000_remove_store_id.rb +28 -0
  29. data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +29 -1
  30. data/lib/openc3/models/activity_model.rb +41 -9
  31. data/lib/openc3/models/auth_model.rb +54 -19
  32. data/lib/openc3/models/cvt_model.rb +2 -265
  33. data/lib/openc3/models/model.rb +16 -0
  34. data/lib/openc3/models/plugin_model.rb +18 -12
  35. data/lib/openc3/models/plugin_store_model.rb +1 -1
  36. data/lib/openc3/models/python_package_model.rb +2 -2
  37. data/lib/openc3/models/queue_model.rb +5 -3
  38. data/lib/openc3/models/script_engine_model.rb +1 -1
  39. data/lib/openc3/models/target_model.rb +75 -42
  40. data/lib/openc3/models/tool_config_model.rb +12 -0
  41. data/lib/openc3/models/tool_model.rb +18 -5
  42. data/lib/openc3/models/trigger_model.rb +1 -1
  43. data/lib/openc3/models/widget_model.rb +2 -9
  44. data/lib/openc3/operators/operator.rb +9 -7
  45. data/lib/openc3/packets/json_packet.rb +2 -0
  46. data/lib/openc3/packets/packet.rb +1 -0
  47. data/lib/openc3/packets/packet_config.rb +28 -12
  48. data/lib/openc3/script/calendar.rb +8 -0
  49. data/lib/openc3/script/script.rb +19 -0
  50. data/lib/openc3/script/storage.rb +6 -6
  51. data/lib/openc3/script/web_socket_api.rb +1 -1
  52. data/lib/openc3/system/system.rb +6 -6
  53. data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +0 -2
  54. data/lib/openc3/top_level.rb +15 -63
  55. data/lib/openc3/topics/command_topic.rb +1 -0
  56. data/lib/openc3/topics/limits_event_topic.rb +1 -1
  57. data/lib/openc3/utilities/authentication.rb +46 -7
  58. data/lib/openc3/utilities/authorization.rb +8 -1
  59. data/lib/openc3/utilities/aws_bucket.rb +2 -3
  60. data/lib/openc3/utilities/bucket_utilities.rb +3 -1
  61. data/lib/openc3/utilities/cli_generator.rb +7 -0
  62. data/lib/openc3/utilities/cmd_log.rb +1 -1
  63. data/lib/openc3/utilities/local_mode.rb +3 -0
  64. data/lib/openc3/utilities/process_manager.rb +1 -1
  65. data/lib/openc3/utilities/python_proxy.rb +11 -4
  66. data/lib/openc3/utilities/questdb_client.rb +764 -2
  67. data/lib/openc3/utilities/running_script.rb +25 -7
  68. data/lib/openc3/utilities/script.rb +452 -0
  69. data/lib/openc3/utilities/secrets.rb +1 -1
  70. data/lib/openc3/version.rb +5 -5
  71. data/templates/conversion/conversion.py +0 -8
  72. data/templates/conversion/conversion.rb +0 -11
  73. data/templates/tool_angular/package.json +2 -2
  74. data/templates/tool_react/package.json +1 -1
  75. data/templates/tool_svelte/package.json +1 -1
  76. data/templates/tool_vue/package.json +3 -3
  77. data/templates/widget/package.json +2 -2
  78. metadata +19 -19
  79. data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +0 -23
  80. data/lib/openc3/migrations/20251213120000_reinstall_plugins.rb +0 -45
@@ -19,6 +19,11 @@ require 'openc3/utilities/local_mode'
19
19
 
20
20
  module OpenC3
21
21
  class ToolConfigModel
22
+ class InvalidNameError < StandardError; end
23
+
24
+ # Allowlist: letters, digits, hyphens, underscores, spaces, and periods
25
+ VALID_NAME_PATTERN = /\A[A-Za-z0-9_\-. ]+\z/
26
+
22
27
  def self.config_tool_names(scope: $openc3_scope)
23
28
  _, keys = Store.scan(0, match: "#{scope}__config__*", type: 'hash', count: 100)
24
29
  # Just return the tool name that is used in the other APIs
@@ -26,19 +31,26 @@ module OpenC3
26
31
  end
27
32
 
28
33
  def self.list_configs(tool, scope: $openc3_scope)
34
+ raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
29
35
  Store.hkeys("#{scope}__config__#{tool}")
30
36
  end
31
37
 
32
38
  def self.load_config(tool, name, scope: $openc3_scope)
39
+ raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
40
+ raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN)
33
41
  Store.hget("#{scope}__config__#{tool}", name)
34
42
  end
35
43
 
36
44
  def self.save_config(tool, name, data, local_mode: true, scope: $openc3_scope)
45
+ raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
46
+ raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN)
37
47
  Store.hset("#{scope}__config__#{tool}", name, data)
38
48
  LocalMode.save_tool_config(scope, tool, name, data) if local_mode
39
49
  end
40
50
 
41
51
  def self.delete_config(tool, name, local_mode: true, scope: $openc3_scope)
52
+ raise InvalidNameError, "Invalid tool name: #{tool}" unless tool.match?(VALID_NAME_PATTERN)
53
+ raise InvalidNameError, "Invalid config name: #{name}" unless name.match?(VALID_NAME_PATTERN)
42
54
  Store.hdel("#{scope}__config__#{tool}", name)
43
55
  LocalMode.delete_tool_config(scope, tool, name) if local_mode
44
56
  end
@@ -16,7 +16,7 @@
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
18
  require 'openc3/models/model'
19
- require 'openc3/models/scope_model'
19
+ # require 'openc3/models/scope_model' # Circular require
20
20
  require 'openc3/utilities/bucket'
21
21
  require 'openc3/utilities/bucket_utilities'
22
22
  require 'rack'
@@ -179,7 +179,7 @@ module OpenC3
179
179
  end
180
180
  end
181
181
 
182
- if @url and !@url.start_with?('/') and @url !~ URI::regexp
182
+ if @url and !@url.start_with?('/') and @url !~ URI::RFC2396_PARSER.make_regexp
183
183
  raise "URL must be a full URL (http://domain.com/path) or a relative path (/path)"
184
184
  end
185
185
 
@@ -250,9 +250,22 @@ module OpenC3
250
250
 
251
251
  variables["tool_name"] = @name
252
252
  start_path = "/tools/#{@folder_name}/"
253
- Dir.glob(gem_path + start_path + "**/*") do |filename|
254
- next if filename == '.' or filename == '..' or File.directory?(filename)
255
-
253
+ # Sort files so dependencies are uploaded before dependents:
254
+ # fonts first, then CSS, then index.html last (it triggers all other loads)
255
+ filenames = Dir.glob(gem_path + start_path + "**/*")
256
+ filenames.reject! { |f| f == '.' or f == '..' or File.directory?(f) }
257
+ filenames.sort_by! do |filename|
258
+ if filename.include?('/fonts/')
259
+ [0, filename]
260
+ elsif filename.include?('/css/')
261
+ [1, filename]
262
+ elsif File.basename(filename) == 'index.html'
263
+ [3, filename]
264
+ else
265
+ [2, filename]
266
+ end
267
+ end
268
+ filenames.each do |filename|
256
269
  key = filename.split(gem_path + '/tools/')[-1]
257
270
  extension = filename.split('.')[-1]
258
271
  content_type = Rack::Mime.mime_type(".#{extension}")
@@ -19,7 +19,7 @@ require 'openc3/models/model'
19
19
  require 'openc3/models/microservice_model'
20
20
  require 'openc3/models/target_model'
21
21
  require 'openc3/models/trigger_group_model'
22
- require 'openc3/models/reaction_model'
22
+ # require 'openc3/models/reaction_model' # Remove circular require
23
23
  require 'openc3/topics/autonomic_topic'
24
24
 
25
25
  module OpenC3
@@ -15,9 +15,8 @@
15
15
  # This file may also be used under the terms of a commercial license
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
- require 'openc3/top_level'
19
18
  require 'openc3/models/model'
20
- require 'openc3/models/scope_model'
19
+ # require 'openc3/models/scope_model' # Circular require
21
20
  require 'openc3/utilities/bucket'
22
21
  require 'openc3/utilities/bucket_utilities'
23
22
 
@@ -126,13 +125,6 @@ module OpenC3
126
125
  end
127
126
 
128
127
  def deploy(gem_path, variables, validate_only: false)
129
- # Ensure tools bucket exists
130
- bucket = nil
131
- unless validate_only
132
- bucket = Bucket.getClient()
133
- bucket.ensure_public(ENV['OPENC3_TOOLS_BUCKET'])
134
- end
135
-
136
128
  filename = gem_path + "/tools/widgets/" + @full_name + '/' + @filename
137
129
 
138
130
  # Load widget file
@@ -146,6 +138,7 @@ module OpenC3
146
138
  unless validate_only
147
139
  cache_control = BucketUtilities.get_cache_control(@filename)
148
140
  # TODO: support widgets that aren't just a single js file (and its associated map file)
141
+ bucket = Bucket.getClient()
149
142
  bucket.put_object(bucket: ENV['OPENC3_TOOLS_BUCKET'], content_type: 'application/javascript', cache_control: cache_control, key: @bucket_key, body: data)
150
143
  data = File.read(filename + '.map', mode: "rb")
151
144
  bucket.put_object(bucket: ENV['OPENC3_TOOLS_BUCKET'], content_type: 'application/json', cache_control: cache_control, key: @bucket_key + '.map', body: data)
@@ -54,8 +54,10 @@ module OpenC3
54
54
  end
55
55
 
56
56
  def finalize
57
- extract()
58
- close()
57
+ unless closed?
58
+ extract()
59
+ close()
60
+ end
59
61
  unlink()
60
62
 
61
63
  output = ''
@@ -174,7 +176,7 @@ module OpenC3
174
176
  end
175
177
  @process.stop
176
178
  end
177
- FileUtils.remove_entry_secure(@temp_dir, true)
179
+ FileUtils.remove_entry_secure(@temp_dir, true) if @temp_dir
178
180
  @process = nil
179
181
  end
180
182
 
@@ -300,6 +302,7 @@ module OpenC3
300
302
  # Respawn process
301
303
  output = p.extract_output
302
304
  Logger.error("Unexpected process died... respawning! #{p.cmd_line}\n#{output}\n", scope: p.scope)
305
+ p.hard_stop
303
306
  p.start
304
307
  end
305
308
  end
@@ -308,6 +311,7 @@ module OpenC3
308
311
 
309
312
  def shutdown_processes(processes)
310
313
  # Make a copy so we don't mutate original
314
+ hard_stop_processes = processes.dup
311
315
  processes = processes.dup
312
316
 
313
317
  Logger.info("Commanding soft stops...")
@@ -331,10 +335,8 @@ module OpenC3
331
335
  end
332
336
  sleep(0.1)
333
337
  end
334
- if processes.length > 0
335
- Logger.debug("Commanding hard stops...")
336
- processes.each { |_name, p| p.hard_stop }
337
- end
338
+ Logger.debug("Commanding hard stops...")
339
+ hard_stop_processes.each { |_name, p| p.output_increment; p.extract_output; p.hard_stop }
338
340
  end
339
341
 
340
342
  def shutdown
@@ -189,6 +189,8 @@ module OpenC3
189
189
  postfix = 'C'
190
190
  when :FORMATTED, :WITH_UNITS
191
191
  postfix = 'F'
192
+ else
193
+ raise "Unsupported value type: #{value_type}"
192
194
  end
193
195
  case reduced_type
194
196
  when :MIN
@@ -16,6 +16,7 @@
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
18
  require 'digest'
19
+ require 'active_support/core_ext/object/deep_dup'
19
20
  require 'openc3/packets/structure'
20
21
  require 'openc3/packets/packet_item'
21
22
  require 'openc3/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT']
@@ -252,7 +252,7 @@ module OpenC3
252
252
  #######################################################################
253
253
  when 'STATE', 'READ_CONVERSION', 'WRITE_CONVERSION', 'POLY_READ_CONVERSION',\
254
254
  'POLY_WRITE_CONVERSION', 'SEG_POLY_READ_CONVERSION', 'SEG_POLY_WRITE_CONVERSION',\
255
- 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'REQUIRED',\
255
+ 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'CONVERTED_DATA', 'REQUIRED',\
256
256
  'LIMITS', 'LIMITS_RESPONSE', 'UNITS', 'FORMAT_STRING', 'DESCRIPTION',\
257
257
  'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY', 'VARIABLE_BIT_SIZE',\
258
258
  'OBFUSCATE'
@@ -675,11 +675,6 @@ module OpenC3
675
675
  klass = OpenC3.require_class(params[0])
676
676
  conversion = klass.new(*params[1..(params.length - 1)])
677
677
  @current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
678
- if klass != ProcessorConversion and (conversion.converted_type.nil? or conversion.converted_bit_size.nil?)
679
- msg = "Read Conversion #{params[0]} on item #{@current_item.name} does not specify converted type or bit size"
680
- @warnings << msg
681
- Logger.instance.warn @warnings[-1]
682
- end
683
678
  else
684
679
  conversion = PythonProxy.new('Conversion', params[0], *params[1..(params.length - 1)])
685
680
  @current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
@@ -719,8 +714,9 @@ module OpenC3
719
714
  # All config.lines following this config.line are considered part
720
715
  # of the conversion until an end of conversion marker is found
721
716
  when 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START'
722
- usage = "#{keyword} <Converted Type (optional)> <Converted Bit Size (optional)>"
723
- parser.verify_num_parameters(0, 2, usage)
717
+ # As of COSMOS 7 the converted type and bit size are deprecated
718
+ # but we're still allowing them to be defined as parameters for backward compatibility
719
+ parser.verify_num_parameters(0, 2, keyword)
724
720
  @proc_text = ''
725
721
  @building_generic_conversion = true
726
722
  parser.set_preserve_lines(true)
@@ -731,10 +727,30 @@ module OpenC3
731
727
  raise parser.error("Invalid converted_type: #{@converted_type}.") unless CONVERTED_DATA_TYPES.include? @converted_type
732
728
  end
733
729
  @converted_bit_size = Integer(params[1]) if params[1]
734
- if @converted_type.nil? or @converted_bit_size.nil?
735
- msg = "Generic Conversion on item #{@current_item.name} does not specify converted type or bit size"
736
- @warnings << msg
737
- Logger.instance.warn @warnings[-1]
730
+
731
+ # Define the converted data type, bit size, and optional array size
732
+ # for items with read conversions (especially DERIVED items)
733
+ when 'CONVERTED_DATA'
734
+ usage = "CONVERTED_DATA <Converted Bit Size> <Converted Type> <Converted Array Size (optional)>"
735
+ parser.verify_num_parameters(2, 3, usage)
736
+ raise parser.error("#{keyword} requires a current item") unless @current_item
737
+ raise parser.error("#{keyword} requires a current item with a conversion") unless @current_item.read_conversion or @current_item.write_conversion
738
+ converted_bit_size = Integer(params[0])
739
+ converted_type = params[1].upcase.intern
740
+ raise parser.error("Invalid converted_type: #{converted_type}.") unless CONVERTED_DATA_TYPES.include? converted_type
741
+ if @current_item.read_conversion
742
+ @current_item.read_conversion.converted_type = converted_type
743
+ @current_item.read_conversion.converted_bit_size = converted_bit_size
744
+ if params[2]
745
+ @current_item.read_conversion.converted_array_size = Integer(params[2])
746
+ end
747
+ end
748
+ if @current_item.write_conversion
749
+ @current_item.write_conversion.converted_type = converted_type
750
+ @current_item.write_conversion.converted_bit_size = converted_bit_size
751
+ if params[2]
752
+ @current_item.write_conversion.converted_array_size = Integer(params[2])
753
+ end
738
754
  end
739
755
 
740
756
  # Define a set of limits for the current telemetry item
@@ -52,6 +52,14 @@ module OpenC3
52
52
  return _cal_handle_response(response, 'Failed to delete timeline')
53
53
  end
54
54
 
55
+ # Creates an activity for the specified timeline.
56
+ #
57
+ # @param name [String] The name of the timeline.
58
+ # @param kind [String] The kind of activity. Must be one of "COMMAND", "SCRIPT", or "RESERVE".
59
+ # @param start [DateTime] The start time of the activity.
60
+ # @param stop [DateTime] The stop time of the activity.
61
+ # @param data [Hash, optional] Additional data to associate with the activity. Defaults to {}. Any activity can provide "username", "notes", and "customTitle". "command", "script", and "reserve" keys are reserves for the corresponding activity kind, with "environment" also available for script activities.
62
+ # @param scope [String, optional] The scope of the activity. Defaults to OPENC3_SCOPE, must correspond to the timeline.
55
63
  def create_timeline_activity(name, kind:, start:, stop:, data: {}, scope: $openc3_scope)
56
64
  kind = kind.to_s.downcase()
57
65
  kinds = %w(command script reserve)
@@ -47,6 +47,9 @@ $disconnect = false
47
47
  $openc3_scope = ENV['OPENC3_SCOPE'] || 'DEFAULT'
48
48
  $openc3_in_cluster = false
49
49
 
50
+ saved_verbose = $VERBOSE
51
+ $VERBOSE = false
52
+
50
53
  module OpenC3
51
54
  module Script
52
55
  private
@@ -177,6 +180,10 @@ module OpenC3
177
180
  message_box(string, *items, **options)
178
181
  end
179
182
 
183
+ def check_box(string, *items, **options)
184
+ message_box(string, *items, **options)
185
+ end
186
+
180
187
  def _file_dialog(title, message, filter:)
181
188
  answer = ''
182
189
  path = "./*"
@@ -199,6 +206,16 @@ module OpenC3
199
206
  _file_dialog(title, message, filter)
200
207
  end
201
208
 
209
+ def open_bucket_dialog(title, message = "Open Bucket File")
210
+ answer = ''
211
+ while answer.empty?
212
+ print "#{title}\n#{message}\n<Type bucket file path (e.g. BUCKET/path/to/file)>:"
213
+ answer = gets
214
+ answer.chomp!
215
+ end
216
+ return answer
217
+ end
218
+
202
219
  def prompt(string, text_color: nil, background_color: nil, font_size: nil, font_family: nil, details: nil)
203
220
  print "#{string}: "
204
221
  print "Details: #{details}\n" if details
@@ -363,3 +380,5 @@ module OpenC3
363
380
  end
364
381
  end
365
382
  end
383
+
384
+ $VERBOSE = saved_verbose
@@ -110,7 +110,7 @@ module OpenC3
110
110
  end
111
111
 
112
112
  return _get_storage_file("#{part}/#{path}", scope: scope)
113
- rescue => e
113
+ rescue
114
114
  if part == "targets_modified"
115
115
  part = "targets"
116
116
  redo
@@ -141,13 +141,13 @@ module OpenC3
141
141
  return result['url']
142
142
  end
143
143
 
144
- def _get_storage_file(path, scope: $openc3_scope)
144
+ def _get_storage_file(path, bucket: 'OPENC3_CONFIG_BUCKET', scope: $openc3_scope)
145
145
  # Create Tempfile to store data
146
146
  file = Tempfile.new('target', binmode: true)
147
147
  file.filename = path
148
148
 
149
149
  endpoint = "/openc3-api/storage/download/#{scope}/#{path}"
150
- result = _get_presigned_request(endpoint, scope: scope)
150
+ result = _get_presigned_request(endpoint, bucket: bucket, scope: scope)
151
151
  puts "Reading #{scope}/#{path}"
152
152
 
153
153
  # Try to get the file
@@ -186,11 +186,11 @@ module OpenC3
186
186
  end
187
187
  end
188
188
 
189
- def _get_presigned_request(endpoint, external: nil, scope: $openc3_scope)
189
+ def _get_presigned_request(endpoint, external: nil, bucket: 'OPENC3_CONFIG_BUCKET', scope: $openc3_scope)
190
190
  if external or !$openc3_in_cluster
191
- response = $api_server.request('get', endpoint, query: { bucket: 'OPENC3_CONFIG_BUCKET' }, scope: scope)
191
+ response = $api_server.request('get', endpoint, query: { bucket: bucket }, scope: scope)
192
192
  else
193
- response = $api_server.request('get', endpoint, query: { bucket: 'OPENC3_CONFIG_BUCKET', internal: true }, scope: scope)
193
+ response = $api_server.request('get', endpoint, query: { bucket: bucket, internal: true }, scope: scope)
194
194
  end
195
195
  if response.nil? || response.status != 201
196
196
  raise "Failed to get presigned URL for #{endpoint}"
@@ -121,7 +121,7 @@ module OpenC3
121
121
  # Connect to the websocket with authorization in query params
122
122
  def connect
123
123
  disconnect()
124
- final_url = @url + "?scope=#{@scope}&authorization=#{@authentication.token(include_bearer: false)}"
124
+ final_url = @url + "?scope=#{@scope}&authorization=#{@authentication.get_otp(scope: @scope)}"
125
125
  @stream = WebSocketClientStream.new(final_url, @write_timeout, @read_timeout, @connect_timeout)
126
126
  @stream.headers = {
127
127
  'Sec-WebSocket-Protocol' => 'actioncable-v1-json, actioncable-unsupported',
@@ -81,22 +81,22 @@ module OpenC3
81
81
  # Nothing to do if there are no targets
82
82
  return if target_names.nil? or target_names.length == 0
83
83
  if @@instance.nil?
84
- FileUtils.mkdir_p("#{base_dir}/targets")
84
+ targets_path = "#{base_dir}/_targets"
85
+ FileUtils.mkdir_p(targets_path)
85
86
  bucket = Bucket.getClient()
86
87
  target_names.each do |target_name|
87
88
  # Retrieve bucket/targets/target_name/<TARGET>_current.zip
88
- zip_path = "#{base_dir}/targets/#{target_name}_current.zip"
89
+ zip_path = "#{targets_path}/#{target_name}_current.zip"
89
90
  FileUtils.mkdir_p(File.dirname(zip_path))
90
91
  bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}_current.zip"
91
92
  Logger.info("Retrieving #{bucket_key} from targets bucket")
92
93
  bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, path: zip_path)
93
- targets_path = "#{base_dir}/targets"
94
- FileUtils.mkdir_p(targets_path)
95
94
  Zip::File.open(zip_path) do |zip_file|
96
95
  zip_file.each do |entry|
97
96
  zip_file.extract(entry.name, destination_directory: targets_path)
98
97
  end
99
98
  end
99
+ FileUtils.rm(zip_path) if File.exist?(zip_path)
100
100
 
101
101
  # Now add any modifications in targets_modified/TARGET/cmd_tlm
102
102
  # This adds support for remembering dynamically created packets
@@ -106,13 +106,13 @@ module OpenC3
106
106
  _, files = bucket.list_files(bucket: ENV['OPENC3_CONFIG_BUCKET'], path: bucket_path)
107
107
  files.each do |file|
108
108
  bucket_key = File.join(bucket_path, file['name'])
109
- local_path = "#{base_dir}/targets/#{target_name}/cmd_tlm/#{file['name']}"
109
+ local_path = "#{targets_path}/#{target_name}/cmd_tlm/#{file['name']}"
110
110
  bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, path: local_path)
111
111
  end
112
112
  end
113
113
 
114
114
  # Build System from targets
115
- System.instance(target_names, "#{base_dir}/targets")
115
+ System.instance(target_names, targets_path)
116
116
  end
117
117
  end
118
118
 
@@ -221,7 +221,6 @@ module OpenC3
221
221
  else
222
222
  Logger.error connect_error.formatted
223
223
  unless @connection_failed_messages.include?(connect_error.message)
224
- OpenC3.write_exception_file(connect_error)
225
224
  @connection_failed_messages << connect_error.message
226
225
  end
227
226
  end
@@ -242,7 +241,6 @@ module OpenC3
242
241
  else
243
242
  Logger.error err.formatted
244
243
  unless @connection_lost_messages.include?(err.message)
245
- OpenC3.write_exception_file(err)
246
244
  @connection_lost_messages << err.message
247
245
  end
248
246
  end
@@ -1,4 +1,4 @@
1
- # encoding: ascii-8bit
1
+ # encoding: utf-8
2
2
 
3
3
  # Copyright 2022 Ball Aerospace & Technologies Corp.
4
4
  # All Rights Reserved.
@@ -101,6 +101,20 @@ module OpenC3
101
101
  end
102
102
  end
103
103
 
104
+ def self.sanitize_path(path)
105
+ return '' if path.nil?
106
+ # path is passed as a parameter thus we have to sanitize it or the code scanner detects:
107
+ # "Uncontrolled data used in path expression"
108
+ # This method is taken directly from the Rails source:
109
+ # https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized
110
+ # NOTE: I removed the '/' character because we have to allow this in order to traverse the path
111
+ sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;\t\r\n\\", "-").gsub('..', '-')
112
+ if sanitized != path
113
+ raise StorageError, "Invalid path: #{path}"
114
+ end
115
+ sanitized
116
+ end
117
+
104
118
  require 'openc3/utilities/logger'
105
119
 
106
120
  # Creates a marshal file by serializing the given obj
@@ -280,65 +294,6 @@ module OpenC3
280
294
  return log_file
281
295
  end
282
296
 
283
- # Writes a log file with information about the current configuration
284
- # including the Ruby version, OpenC3 version, whether you are on Windows, the
285
- # OpenC3 path, and the Ruby path along with the exception that
286
- # is passed in.
287
- #
288
- # @param [String] filename String to append to the exception log filename.
289
- # The filename will start with a date/time stamp.
290
- # @param [String] log_dir By default this method will write to the OpenC3
291
- # default log directory. By setting this parameter you can override the
292
- # directory the log will be written to.
293
- # @return [String|nil] The fully pathed log filename or nil if there was
294
- # an error creating the log file.
295
- def self.write_exception_file(exception, filename = 'exception', log_dir = nil)
296
- log_file = create_log_file(filename, log_dir) do |file|
297
- file.puts "Exception:"
298
- if exception
299
- file.puts exception.formatted
300
- file.puts
301
- else
302
- file.puts "No Exception Given"
303
- file.puts caller.join("\n")
304
- file.puts
305
- end
306
- file.puts "Caller Backtrace:"
307
- file.puts caller().join("\n")
308
- file.puts
309
-
310
- file.puts "Ruby Version: ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE} patchlevel #{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]"
311
- file.puts "Rubygems Version: #{Gem::VERSION}"
312
- file.puts "OpenC3 Version: #{OpenC3::VERSION}"
313
- file.puts "OpenC3::PATH: #{OpenC3::PATH}"
314
- file.puts ""
315
- file.puts "Environment:"
316
- file.puts "RUBYOPT: #{ENV['RUBYOPT']}"
317
- file.puts "RUBYLIB: #{ENV['RUBYLIB']}"
318
- file.puts "GEM_PATH: #{ENV['GEM_PATH']}"
319
- file.puts "GEMRC: #{ENV['GEMRC']}"
320
- file.puts "RI_DEVKIT: #{ENV['RI_DEVKIT']}"
321
- file.puts "GEM_HOME: #{ENV['GEM_HOME']}"
322
- file.puts "PYTHONUSERBASE: #{ENV['PYTHONUSERBASE']}"
323
- file.puts "PATH: #{ENV['PATH']}"
324
- file.puts ""
325
- file.puts "Ruby Path:\n #{$:.join("\n ")}\n\n"
326
- file.puts "Gems:"
327
- Gem.loaded_specs.values.map { |x| file.puts "#{x.name} #{x.version} #{x.platform}" }
328
- file.puts ""
329
- file.puts "All Threads Backtraces:"
330
- Thread.list.each do |thread|
331
- file.puts thread.backtrace.join("\n")
332
- file.puts
333
- end
334
- file.puts ""
335
- file.puts ""
336
- ensure
337
- file.close
338
- end
339
- return log_file
340
- end
341
-
342
297
  # Writes a log file with information about unexpected output
343
298
  #
344
299
  # @param [String] text The unexpected output text
@@ -367,7 +322,6 @@ module OpenC3
367
322
  def self.handle_fatal_exception(error, _try_gui = true)
368
323
  unless SystemExit === error or SignalException === error
369
324
  $openc3_fatal_exception = error
370
- self.write_exception_file(error)
371
325
  Logger.fatal "Fatal Exception! Exiting..."
372
326
  Logger.fatal error.formatted
373
327
  if $stdout != STDOUT
@@ -392,7 +346,6 @@ module OpenC3
392
346
  # @param try_gui [Boolean] Whether to try and create a GUI exception popup
393
347
  def self.handle_critical_exception(error, _try_gui = true)
394
348
  Logger.error "Critical Exception! #{error.formatted}"
395
- self.write_exception_file(error)
396
349
  end
397
350
 
398
351
  # Creates a Ruby Thread to run the given block. Rescues any exceptions and
@@ -412,7 +365,6 @@ module OpenC3
412
365
  Logger.error e.formatted
413
366
  retry_count += 1
414
367
  if retry_count <= retry_attempts
415
- self.write_exception_file(e)
416
368
  retry
417
369
  end
418
370
  handle_fatal_exception(e)
@@ -21,6 +21,7 @@ require 'openc3/utilities/open_telemetry'
21
21
 
22
22
  module OpenC3
23
23
  class CommandTopic < Topic
24
+ # TODO: This is in several places, should maybe be a parameter in settings?
24
25
  COMMAND_ACK_TIMEOUT_S = 30
25
26
 
26
27
  def self.write_packet(packet, scope:)
@@ -53,7 +53,7 @@ module OpenC3
53
53
  limits['red_high'] = event[:red_high]
54
54
  limits['green_low'] = event[:green_low] if event[:green_low] && event[:green_high]
55
55
  limits['green_high'] = event[:green_high] if event[:green_low] && event[:green_high]
56
- limits_settings[event[:limits_set]] = limits
56
+ limits_settings[event[:limits_set].to_s] = limits
57
57
  limits_settings['persistence_setting'] = event[:persistence] if event[:persistence]
58
58
  limits_settings['enabled'] = event[:enabled] if not event[:enabled].nil?
59
59
  Store.hset("#{scope}__current_limits_settings", field, JSON.generate(limits_settings, allow_nan: true))
@@ -33,8 +33,10 @@ module OpenC3
33
33
  raise OpenC3AuthenticationError, "Authentication requires environment variable OPENC3_API_PASSWORD"
34
34
  end
35
35
  @service = password == ENV['OPENC3_SERVICE_PASSWORD']
36
- response = _make_auth_request(password)
37
- @token = response.body
36
+ retry_faraday_request do
37
+ response = _make_auth_request(password)
38
+ @token = response.body
39
+ end
38
40
  if @token.nil? or @token.empty?
39
41
  raise OpenC3AuthenticationError, "Authentication failed. Please check the password in the environment variable OPENC3_API_PASSWORD"
40
42
  end
@@ -45,23 +47,60 @@ module OpenC3
45
47
  @token
46
48
  end
47
49
 
50
+ def get_otp(scope: 'DEFAULT')
51
+ session_token = token()
52
+ if session_token.nil? or session_token.empty?
53
+ raise OpenC3AuthenticationError, "Uninitialized authentication: unable to get OTP"
54
+ end
55
+ retry_faraday_request do
56
+ response = _make_otp_request(scope: scope)
57
+ return response.body
58
+ end
59
+ end
60
+
48
61
  def _make_auth_request(password)
49
62
  Faraday.new.post(_generate_auth_url, '{"password": "' + password + '"}', {'Content-Type' => 'application/json'})
50
63
  end
51
64
 
52
- def _generate_auth_url
65
+ def _make_otp_request(scope: 'DEFAULT')
66
+ params = {
67
+ 'scope' => scope
68
+ }
69
+ headers = {
70
+ 'Authorization' => token,
71
+ }
72
+ Faraday.new.get(_generate_auth_url('/auth/otp'), params, headers)
73
+ end
74
+
75
+ def _generate_auth_url(endpoint = nil)
53
76
  schema = ENV['OPENC3_API_SCHEMA'] || 'http'
54
77
  hostname = ENV['OPENC3_API_HOSTNAME'] || (ENV['OPENC3_DEVEL'] ? '127.0.0.1' : 'openc3-cosmos-cmd-tlm-api')
55
78
  port = ENV['OPENC3_API_PORT'] || '2901'
56
79
  port = port.to_i
57
- endpoint = if @service
58
- "auth/verify_service"
59
- else
60
- "auth/verify"
80
+ unless endpoint
81
+ endpoint = if @service
82
+ "auth/verify_service"
83
+ else
84
+ "auth/verify"
85
+ end
61
86
  end
62
87
  return "#{schema}://#{hostname}:#{port}/openc3-api/#{endpoint}"
63
88
  end
64
89
 
90
+ def retry_faraday_request(max_retries: 3)
91
+ retries = 0
92
+ begin
93
+ yield
94
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
95
+ retries += 1
96
+ if retries <= max_retries
97
+ STDOUT.puts "Authentication request failed (attempt #{retries}/3): #{e.message}. Retrying in #{retries}s..."
98
+ sleep(retries)
99
+ retry
100
+ end
101
+ raise
102
+ end
103
+ end
65
104
  end
66
105
 
67
106
  # OpenC3 enterprise Keycloak authentication code