openc3 5.12.0 → 5.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of openc3 might be problematic. Click here for more details.

Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -3
  3. data/bin/openc3cli +21 -18
  4. data/data/config/command_modifiers.yaml +53 -1
  5. data/data/config/graph_settings.yaml +1 -1
  6. data/data/config/item_modifiers.yaml +1 -2
  7. data/data/config/parameter_modifiers.yaml +13 -14
  8. data/data/config/screen.yaml +1 -2
  9. data/data/config/target_config.yaml +2 -6
  10. data/lib/openc3/accessors/accessor.rb +42 -29
  11. data/lib/openc3/accessors/binary_accessor.rb +11 -1
  12. data/lib/openc3/accessors/form_accessor.rb +11 -1
  13. data/lib/openc3/accessors/http_accessor.rb +38 -0
  14. data/lib/openc3/accessors/json_accessor.rb +15 -3
  15. data/lib/openc3/accessors/template_accessor.rb +150 -0
  16. data/lib/openc3/accessors/xml_accessor.rb +11 -1
  17. data/lib/openc3/accessors.rb +1 -0
  18. data/lib/openc3/api/cmd_api.rb +99 -35
  19. data/lib/openc3/api/limits_api.rb +3 -3
  20. data/lib/openc3/api/tlm_api.rb +70 -31
  21. data/lib/openc3/interfaces/interface.rb +9 -7
  22. data/lib/openc3/interfaces/mqtt_interface.rb +11 -9
  23. data/lib/openc3/interfaces/mqtt_stream_interface.rb +78 -0
  24. data/lib/openc3/interfaces/protocols/cmd_response_protocol.rb +116 -0
  25. data/lib/openc3/interfaces/tcpip_client_interface.rb +4 -0
  26. data/lib/openc3/interfaces/tcpip_server_interface.rb +5 -0
  27. data/lib/openc3/interfaces.rb +1 -1
  28. data/lib/openc3/logs/packet_log_reader.rb +2 -2
  29. data/lib/openc3/logs/text_log_writer.rb +3 -2
  30. data/lib/openc3/microservices/decom_microservice.rb +1 -0
  31. data/lib/openc3/microservices/interface_microservice.rb +10 -1
  32. data/lib/openc3/microservices/trigger_group_microservice.rb +2 -1
  33. data/lib/openc3/models/cvt_model.rb +16 -12
  34. data/lib/openc3/models/gem_model.rb +20 -3
  35. data/lib/openc3/models/microservice_model.rb +1 -1
  36. data/lib/openc3/models/plugin_model.rb +43 -5
  37. data/lib/openc3/models/target_model.rb +69 -8
  38. data/lib/openc3/packets/json_packet.rb +46 -15
  39. data/lib/openc3/packets/packet.rb +92 -4
  40. data/lib/openc3/packets/packet_config.rb +27 -2
  41. data/lib/openc3/packets/parsers/xtce_parser.rb +5 -1
  42. data/lib/openc3/script/api_shared.rb +42 -31
  43. data/lib/openc3/script/commands.rb +18 -12
  44. data/lib/openc3/script/limits.rb +1 -1
  45. data/lib/openc3/script/script.rb +6 -12
  46. data/lib/openc3/script/storage.rb +4 -4
  47. data/lib/openc3/script/web_socket_api.rb +2 -2
  48. data/lib/openc3/streams/mqtt_stream.rb +109 -0
  49. data/lib/openc3/streams/tcpip_socket_stream.rb +19 -0
  50. data/lib/openc3/system/system.rb +13 -1
  51. data/lib/openc3/utilities/cli_generator.rb +48 -21
  52. data/lib/openc3/utilities/local_mode.rb +3 -3
  53. data/lib/openc3/utilities/logger.rb +17 -16
  54. data/lib/openc3/utilities/process_manager.rb +1 -1
  55. data/lib/openc3/utilities/store_queued.rb +126 -0
  56. data/lib/openc3/version.rb +5 -5
  57. data/templates/conversion/conversion.py +28 -0
  58. data/templates/conversion/conversion.rb +1 -18
  59. data/templates/limits_response/response.py +37 -0
  60. data/templates/limits_response/response.rb +0 -17
  61. data/templates/microservice/microservices/TEMPLATE/microservice.py +54 -0
  62. data/templates/microservice/microservices/TEMPLATE/microservice.rb +0 -7
  63. data/templates/plugin/.gitignore +1 -0
  64. data/templates/plugin/plugin.gemspec +2 -2
  65. data/templates/target/targets/TARGET/lib/target.py +9 -0
  66. data/templates/target/targets/TARGET/procedures/procedure.py +3 -0
  67. data/templates/target/targets/TARGET/public/README.txt +1 -0
  68. data/templates/tool_angular/package.json +21 -20
  69. data/templates/tool_angular/yarn.lock +2287 -3171
  70. data/templates/tool_react/package.json +15 -15
  71. data/templates/tool_react/yarn.lock +716 -789
  72. data/templates/tool_svelte/package.json +16 -15
  73. data/templates/tool_svelte/src/services/openc3-api.js +17 -22
  74. data/templates/tool_svelte/yarn.lock +715 -620
  75. data/templates/tool_vue/package.json +16 -15
  76. data/templates/tool_vue/yarn.lock +149 -69
  77. data/templates/widget/package.json +15 -14
  78. data/templates/widget/yarn.lock +132 -63
  79. metadata +160 -148
  80. data/lib/openc3/io/openc3_snmp.rb +0 -61
@@ -198,14 +198,16 @@ module OpenC3
198
198
  log_dont_log.upcase!
199
199
  period = "#{period.to_f}s"
200
200
  @scheduler.every period do
201
- begin
202
- if log_dont_log == 'DONT_LOG'
203
- cmd(cmd_string, log_message: false)
204
- else
205
- cmd(cmd_string)
201
+ if connected?()
202
+ begin
203
+ if log_dont_log == 'DONT_LOG'
204
+ cmd(cmd_string, log_message: false)
205
+ else
206
+ cmd(cmd_string)
207
+ end
208
+ rescue Exception => err
209
+ Logger.error("Error sending periodic cmd(#{cmd_string}):\n#{err.formatted}")
206
210
  end
207
- rescue Exception => err
208
- Logger.error("Error sending periodic cmd(#{cmd_string}):\n#{err.formatted}")
209
211
  end
210
212
  end
211
213
  end
@@ -161,8 +161,8 @@ module OpenC3
161
161
  end
162
162
 
163
163
  def read
164
- topic = @read_topics.shift
165
164
  packet = super()
165
+ topic = @read_topics.shift
166
166
  return nil unless packet
167
167
  identified_packet = @read_packets_by_topic[topic]
168
168
  if identified_packet
@@ -175,15 +175,17 @@ module OpenC3
175
175
  end
176
176
 
177
177
  def write(packet)
178
- topics = packet.meta['TOPIC']
179
- topics = packet.meta['TOPICS'] unless topics
180
- if topics
181
- topics.each do |topic|
182
- @write_topics << topic
183
- super(packet)
178
+ @write_mutex.synchronize do
179
+ topics = packet.meta['TOPIC']
180
+ topics = packet.meta['TOPICS'] unless topics
181
+ if topics
182
+ topics.each do |topic|
183
+ @write_topics << topic
184
+ super(packet)
185
+ end
186
+ else
187
+ raise "Command packet #{packet.target_name} #{packet.packet_name} requires a META TOPIC or TOPICS"
184
188
  end
185
- else
186
- raise "Command packet #{packet.target_name} #{packet.packet_name} requires a META TOPIC or TOPICS"
187
189
  end
188
190
  end
189
191
 
@@ -0,0 +1,78 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2023 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/interfaces/stream_interface'
20
+ require 'openc3/streams/mqtt_stream'
21
+
22
+ module OpenC3
23
+ class MqttStreamInterface < StreamInterface
24
+ # @param hostname [String] MQTT server to connect to
25
+ # @param port [Integer] MQTT port
26
+ # @param ssl [Boolean] Use SSL true/false
27
+ def initialize(hostname, port = 1883, ssl = false, write_topic = nil, read_topic = nil, protocol_type = nil, *protocol_args)
28
+ super(protocol_type, protocol_args)
29
+ @hostname = hostname
30
+ @port = Integer(port)
31
+ @ssl = ConfigParser.handle_true_false(ssl)
32
+ @write_topic = ConfigParser.handle_nil(write_topic)
33
+ @read_topic = ConfigParser.handle_nil(read_topic)
34
+ @username = nil
35
+ @password = nil
36
+ @cert = nil
37
+ @key = nil
38
+ @ca_file = nil
39
+ end
40
+
41
+ # Creates a new {SerialStream} using the parameters passed in the constructor
42
+ def connect
43
+ @stream = MqttStream.new(@hostname, @port, @ssl, @write_topic, @read_topic)
44
+ @stream.username = @username if @username
45
+ @stream.password = @password if @password
46
+ @stream.cert = @cert if @cert
47
+ @stream.key = @key if @key
48
+ @stream.ca_file = @ca_file if @ca_file
49
+ super()
50
+ end
51
+
52
+ # Supported Options
53
+ # USERNAME - Username for Mqtt Server
54
+ # PASSWORD - Password for Mqtt Server
55
+ # CERT - Public Key for Client Cert Auth
56
+ # KEY - Private Key for Client Cert Auth
57
+ # CA_FILE - Certificate Authority for Client Cert Auth
58
+ # (see Interface#set_option)
59
+ def set_option(option_name, option_values)
60
+ super(option_name, option_values)
61
+ case option_name.upcase
62
+ when 'USERNAME'
63
+ @username = option_values[0]
64
+ when 'PASSWORD'
65
+ @password = option_values[0]
66
+ when 'CERT'
67
+ @cert = option_values[0]
68
+ when 'KEY'
69
+ @key = option_values[0]
70
+ when 'CA_FILE'
71
+ # CA_FILE must be given as a file
72
+ @ca_file = Tempfile.new('ca_file')
73
+ @ca_file.write(option_values[0])
74
+ @ca_file.close
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,116 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2024 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/config/config_parser'
20
+ require 'openc3/interfaces/protocols/protocol'
21
+ require 'thread' # For Queue
22
+ require 'timeout' # For Timeout::Error
23
+
24
+ module OpenC3
25
+ # Protocol that waits for a response for any commands with a defined response packet.
26
+ # The response packet is identified but not defined by the protocol.
27
+ class CmdResponseProtocol < Protocol
28
+ # @param response_timeout [Float] Number of seconds to wait before timing out
29
+ # when waiting for a response
30
+ # @param response_polling_period [Float] Number of seconds to wait between polling
31
+ # for a response
32
+ # @param raise_exceptions [String] Whether to raise exceptions when errors
33
+ # occur in the protocol like unexpected responses or response timeouts.
34
+ # @param allow_empty_data [true/false/nil] See Protocol#initialize
35
+ def initialize(
36
+ response_timeout = 5.0,
37
+ response_polling_period = 0.02,
38
+ raise_exceptions = false,
39
+ allow_empty_data = nil
40
+ )
41
+ super(allow_empty_data)
42
+ @response_timeout = ConfigParser.handle_nil(response_timeout)
43
+ @response_timeout = @response_timeout.to_f if @response_timeout
44
+ @response_polling_period = response_polling_period.to_f
45
+ @raise_exceptions = ConfigParser.handle_true_false(raise_exceptions)
46
+ @write_block_queue = Queue.new
47
+ @response_packet = nil
48
+ end
49
+
50
+ def connect_reset
51
+ super()
52
+ @write_block_queue.clear
53
+ end
54
+
55
+ def disconnect_reset
56
+ super()
57
+ @write_block_queue << nil # Unblock the write block queue
58
+ end
59
+
60
+ def read_packet(packet)
61
+ if @response_packet
62
+ # Grab the response packet specified in the command
63
+ result_packet = System.telemetry.packet(@response_packet[0], @response_packet[1]).clone
64
+ result_packet.buffer = packet.buffer
65
+ result_packet.received_time = nil
66
+ result_packet.stored = packet.stored
67
+ result_packet.extra = packet.extra
68
+
69
+ # Release the write
70
+ @write_block_queue << nil
71
+
72
+ # This returns the fully identified and defined packet
73
+ # Received time is handled by the interface microservice
74
+ return result_packet
75
+ else
76
+ return packet
77
+ end
78
+ end
79
+
80
+ def write_packet(packet)
81
+ # Setup the response packet (if there is one)
82
+ # This primes waiting for the response in post_write_interface
83
+ @response_packet = packet.response
84
+
85
+ return packet
86
+ end
87
+
88
+ def post_write_interface(packet, data, extra = nil)
89
+ if @response_packet
90
+ if @response_timeout
91
+ response_timeout_time = Time.now + @response_timeout
92
+ else
93
+ response_timeout_time = nil
94
+ end
95
+
96
+ # Block the write until the response is received
97
+ begin
98
+ @write_block_queue.pop(true)
99
+ rescue
100
+ sleep(@response_polling_period)
101
+ retry if !response_timeout_time
102
+ retry if response_timeout_time and Time.now < response_timeout_time
103
+ handle_error("#{@interface ? @interface.name : ""}: Timeout waiting for response")
104
+ end
105
+
106
+ @response_packet = nil
107
+ end
108
+ return super(packet, data, extra)
109
+ end
110
+
111
+ def handle_error(msg)
112
+ Logger.error(msg)
113
+ raise msg if @raise_exceptions
114
+ end
115
+ end
116
+ end
@@ -67,6 +67,10 @@ module OpenC3
67
67
  @write_timeout,
68
68
  @read_timeout
69
69
  )
70
+ # Pass down options to the stream
71
+ @options.each do |option_name, option_values|
72
+ @stream.set_option(option_name, option_values)
73
+ end
70
74
  super()
71
75
  end
72
76
  end
@@ -391,6 +391,11 @@ module OpenC3
391
391
  read_socket = socket if listen_read
392
392
  stream = TcpipSocketStream.new(write_socket, read_socket, @write_timeout, @read_timeout)
393
393
 
394
+ # Pass down options to the stream
395
+ @options.each do |option_name, option_values|
396
+ stream.set_option(option_name, option_values)
397
+ end
398
+
394
399
  interface = StreamInterface.new
395
400
  interface.target_names = @target_names
396
401
  interface.cmd_target_names = @cmd_target_names
@@ -42,7 +42,7 @@ module OpenC3
42
42
  autoload(:PreidentifiedProtocol, 'openc3/interfaces/protocols/preidentified_protocol.rb')
43
43
  autoload(:TemplateProtocol, 'openc3/interfaces/protocols/template_protocol.rb')
44
44
  autoload(:TerminatedProtocol, 'openc3/interfaces/protocols/terminated_protocol.rb')
45
-
45
+ autoload(:CmdResponseProtocol, 'openc3/interfaces/protocols/cmd_response_protocol.rb')
46
46
  autoload(:CrcProtocol, 'openc3/interfaces/protocols/crc_protocol.rb')
47
47
  autoload(:IgnorePacketProtocol, 'openc3/interfaces/protocols/ignore_packet_protocol.rb')
48
48
  end
@@ -298,12 +298,12 @@ module OpenC3
298
298
  next_offset = 12
299
299
  received_time_nsec_since_epoch = time_nsec_since_epoch
300
300
  if includes_received_time
301
- received_time_nsec_since_epoch = entry[next_offset..(next_offset + OPENC3_RECEIVED_TIME_FIXED_SIZE - 1)].unpack(OPENC3_RECEIVED_TIME_PACK_DIRECTIVE)
301
+ received_time_nsec_since_epoch = entry[next_offset..(next_offset + OPENC3_RECEIVED_TIME_FIXED_SIZE - 1)].unpack(OPENC3_RECEIVED_TIME_PACK_DIRECTIVE)[0]
302
302
  next_offset += OPENC3_RECEIVED_TIME_FIXED_SIZE
303
303
  end
304
304
  extra = nil
305
305
  if includes_extra
306
- extra_length = entry[next_offset..(next_offset + OPENC3_EXTRA_LENGTH_FIXED_SIZE - 1)].unpack(OPENC3_EXTRA_LENGTH_PACK_DIRECTIVE)
306
+ extra_length = entry[next_offset..(next_offset + OPENC3_EXTRA_LENGTH_FIXED_SIZE - 1)].unpack(OPENC3_EXTRA_LENGTH_PACK_DIRECTIVE)[0]
307
307
  next_offset += OPENC3_EXTRA_LENGTH_FIXED_SIZE
308
308
  extra_encoded = entry[next_offset..(next_offset + extra_length - 1)]
309
309
  next_offset += extra_length
@@ -78,8 +78,9 @@ module OpenC3
78
78
  begin
79
79
  # Need to write the OFFSET_MARKER for each packet
80
80
  @last_offsets.each do |redis_topic, last_offset|
81
- time = Time.now
82
- data = { time: time.to_nsec_from_epoch, '@timestamp' => time.xmlschema(3), severity: 'INFO', "microservice_name" => Logger.microservice_name, "container_name" => @container_name, "last_offset" => last_offset, "redis_topic" => redis_topic, "type" => "offset" }
81
+ time = Time.now.utc
82
+ # timestamp iso8601 with 6 decimal places to match the python output format
83
+ data = { time: time.to_nsec_from_epoch, '@timestamp' => time.iso8601(6), level: 'INFO', "microservice_name" => Logger.microservice_name, "container_name" => @container_name, "last_offset" => last_offset, "redis_topic" => redis_topic, "type" => "offset" }
83
84
  write_entry(time.to_nsec_from_epoch, data.as_json(allow_nan: true).to_json(allow_nan: true)) if @file
84
85
  end
85
86
 
@@ -100,6 +100,7 @@ module OpenC3
100
100
  packet.extra = extra
101
101
  end
102
102
  packet.buffer = msg_hash["buffer"]
103
+ packet.process # Run processors
103
104
  packet.check_limits(System.limits_set) # Process all the limits and call the limits_change_callback (as necessary)
104
105
 
105
106
  TelemetryDecomTopic.write_packet(packet, scope: @scope)
@@ -613,7 +613,16 @@ module OpenC3
613
613
 
614
614
  def connect
615
615
  @logger.info "#{@interface.name}: Connecting ..."
616
- @interface.connect
616
+ begin
617
+ @interface.connect
618
+ rescue Exception => error
619
+ begin
620
+ @interface.disconnect # Ensure disconnect is called at least once on a partial connect
621
+ rescue Exception
622
+ # We want to report any connect errors, not disconnect in this case
623
+ end
624
+ raise error
625
+ end
617
626
  @interface.state = 'CONNECTED'
618
627
  if @interface_or_router == 'INTERFACE'
619
628
  InterfaceStatusModel.set(@interface.as_json(:allow_nan => true), scope: @scope)
@@ -333,7 +333,8 @@ module OpenC3
333
333
  )
334
334
  return nil if packet.nil?
335
335
  _, limit = packet.read_with_limits_state(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
336
- return limit
336
+ # Convert limit symbol to string since we'll be comparing with strings
337
+ return limit.to_s
337
338
  end
338
339
 
339
340
  # extract the value outlined in the operand to get the packet item value
@@ -52,12 +52,14 @@ module OpenC3
52
52
 
53
53
  # Get the hash for packet in the CVT
54
54
  # Note: Does not apply overrides
55
- def self.get(target_name:, packet_name:, cache_timeout: 0.1, scope: $openc3_scope)
55
+ def self.get(target_name:, packet_name:, cache_timeout: nil, scope: $openc3_scope)
56
56
  key = "#{scope}__tlm__#{target_name}"
57
57
  tgt_pkt_key = key + "__#{packet_name}"
58
- cache_time, hash = @@packet_cache[tgt_pkt_key]
59
58
  now = Time.now
60
- return hash if hash and (now - cache_time) < cache_timeout
59
+ if cache_timeout
60
+ cache_time, hash = @@packet_cache[tgt_pkt_key]
61
+ return hash if hash and (now - cache_time) < cache_timeout
62
+ end
61
63
  packet = Store.hget(key, packet_name)
62
64
  raise "Packet '#{target_name} #{packet_name}' does not exist" unless packet
63
65
  hash = JSON.parse(packet, :allow_nan => true, :create_additions => true)
@@ -67,7 +69,7 @@ module OpenC3
67
69
 
68
70
  # Set an item in the current value table
69
71
  def self.set_item(target_name, packet_name, item_name, value, type:, scope: $openc3_scope)
70
- hash = get(target_name: target_name, packet_name: packet_name, cache_timeout: 0.0, scope: scope)
72
+ hash = get(target_name: target_name, packet_name: packet_name, cache_timeout: nil, scope: scope)
71
73
  case type
72
74
  when :WITH_UNITS
73
75
  hash["#{item_name}__U"] = value.to_s # WITH_UNITS should always be a string
@@ -89,10 +91,10 @@ module OpenC3
89
91
  end
90
92
 
91
93
  # Get an item from the current value table
92
- def self.get_item(target_name, packet_name, item_name, type:, cache_timeout: 0.1, scope: $openc3_scope)
94
+ def self.get_item(target_name, packet_name, item_name, type:, cache_timeout: nil, scope: $openc3_scope)
93
95
  result, types = self._handle_item_override(target_name, packet_name, item_name, type: type, cache_timeout: cache_timeout, scope: scope)
94
96
  return result if result
95
- hash = get(target_name: target_name, packet_name: packet_name, scope: scope)
97
+ hash = get(target_name: target_name, packet_name: packet_name, cache_timeout: cache_timeout, scope: scope)
96
98
  hash.values_at(*types).each do |result|
97
99
  if result
98
100
  if type == :FORMATTED or type == :WITH_UNITS
@@ -109,7 +111,7 @@ module OpenC3
109
111
  # @param items [Array<String>] Items to return. Must be formatted as TGT__PKT__ITEM__TYPE
110
112
  # @param stale_time [Integer] Time in seconds from Time.now that value will be marked stale
111
113
  # @return [Array] Array of values
112
- def self.get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, scope: $openc3_scope)
114
+ def self.get_tlm_values(items, stale_time: 30, cache_timeout: nil, scope: $openc3_scope)
113
115
  now = Time.now
114
116
  results = []
115
117
  lookups = []
@@ -246,7 +248,7 @@ module OpenC3
246
248
  end
247
249
  end
248
250
 
249
- def self.determine_latest_packet_for_item(target_name, item_name, cache_timeout: 0.1, scope: $openc3_scope)
251
+ def self.determine_latest_packet_for_item(target_name, item_name, cache_timeout: nil, scope: $openc3_scope)
250
252
  item_map = TargetModel.get_item_to_packet_map(target_name, scope: scope)
251
253
  packet_names = item_map[item_name]
252
254
  raise "Item '#{target_name} LATEST #{item_name}' does not exist for scope: #{scope}" unless packet_names
@@ -293,10 +295,12 @@ module OpenC3
293
295
  end
294
296
 
295
297
  def self._get_overrides(now, tgt_pkt_key, overrides, target_name, packet_name, cache_timeout:, scope:)
296
- cache_time, hash = @@override_cache[tgt_pkt_key]
297
- if hash and (now - cache_time) < cache_timeout
298
- overrides[tgt_pkt_key] = hash
299
- return hash
298
+ if cache_timeout
299
+ cache_time, hash = @@override_cache[tgt_pkt_key]
300
+ if hash and (now - cache_time) < cache_timeout
301
+ overrides[tgt_pkt_key] = hash
302
+ return hash
303
+ end
300
304
  end
301
305
  override_data = Store.hget("#{scope}__override__#{target_name}", packet_name)
302
306
  if override_data
@@ -39,11 +39,17 @@ module OpenC3
39
39
  include Api
40
40
 
41
41
  def self.names
42
- result = Pathname.new("#{ENV['GEM_HOME']}/gems").children.select { |c| c.directory? }.collect { |p| File.basename(p) + '.gem' }
42
+ if Dir.exist?("#{ENV['GEM_HOME']}/gems")
43
+ result = Pathname.new("#{ENV['GEM_HOME']}/gems").children.select { |c| c.directory? }.collect { |p| File.basename(p) + '.gem' }
44
+ else
45
+ result = []
46
+ end
43
47
  return result.sort
44
48
  end
45
49
 
46
50
  def self.get(name)
51
+ path = "#{ENV['GEM_HOME']}/cosmoscache/#{name}"
52
+ return path if File.exist?(path)
47
53
  path = "#{ENV['GEM_HOME']}/cache/#{name}"
48
54
  return path if File.exist?(path)
49
55
  raise "Gem #{name} not found"
@@ -52,8 +58,9 @@ module OpenC3
52
58
  def self.put(gem_file_path, gem_install: true, scope:)
53
59
  if File.file?(gem_file_path)
54
60
  gem_filename = File.basename(gem_file_path)
55
- FileUtils.mkdir_p("#{ENV['GEM_HOME']}/cache") unless Dir.exist?("#{ENV['GEM_HOME']}/cache")
56
- FileUtils.cp(gem_file_path, "#{ENV['GEM_HOME']}/cache/#{File.basename(gem_file_path)}")
61
+ # Put into cosmoscache folder that we control
62
+ FileUtils.mkdir_p("#{ENV['GEM_HOME']}/cosmoscache") unless Dir.exist?("#{ENV['GEM_HOME']}/cosmoscache")
63
+ FileUtils.cp(gem_file_path, "#{ENV['GEM_HOME']}/cosmoscache/#{File.basename(gem_file_path)}")
57
64
  if gem_install
58
65
  Logger.info "Installing gem: #{gem_filename}"
59
66
  result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "geminstall", gem_filename, scope], "package_install", gem_filename, Time.now + 3600.0, scope: scope)
@@ -118,5 +125,15 @@ module OpenC3
118
125
  version = File.basename(split_name[-1], '.gem')
119
126
  return gem_name, version
120
127
  end
128
+
129
+ def self.destroy_all_other_versions(name)
130
+ keep_gem_name, keep_gem_version = GemModel.extract_name_and_version(name)
131
+ GemModel.names.each do |gem_full_name|
132
+ gem_name, gem_version = GemModel.extract_name_and_version(gem_full_name)
133
+ if gem_name == keep_gem_name and gem_version != keep_gem_version
134
+ GemModel.destroy(gem_full_name)
135
+ end
136
+ end
137
+ end
121
138
  end
122
139
  end
@@ -185,7 +185,7 @@ module OpenC3
185
185
  @topics << parameters[0]
186
186
  when 'TARGET_NAME'
187
187
  parser.verify_num_parameters(1, 1, "#{keyword} <Target Name>")
188
- @target_names << parameters[0]
188
+ @target_names << parameters[0].upcase
189
189
  when 'CMD'
190
190
  parser.verify_num_parameters(1, nil, "#{keyword} <Args>")
191
191
  @cmd = parameters.dup
@@ -158,6 +158,11 @@ module OpenC3
158
158
  gem_file_path = OpenC3::GemModel.get(gem_name)
159
159
  end
160
160
 
161
+ # Attempt to remove all older versions of this same plugin before install to prevent version conflicts
162
+ # Especially on downgrades
163
+ # Leave the same version if it already exists
164
+ OpenC3::GemModel.destroy_all_other_versions(File.basename(gem_file_path))
165
+
161
166
  # Actually install the gem now (slow)
162
167
  OpenC3::GemModel.install(gem_file_path, scope: scope) unless validate_only
163
168
 
@@ -296,10 +301,15 @@ module OpenC3
296
301
 
297
302
  # Undeploy all models associated with this plugin
298
303
  def undeploy
304
+ errors = []
299
305
  microservice_count = 0
300
306
  microservices = MicroserviceModel.find_all_by_plugin(plugin: @name, scope: @scope)
301
307
  microservices.each do |name, model_instance|
302
- model_instance.destroy
308
+ begin
309
+ model_instance.destroy
310
+ rescue Exception => error
311
+ errors << error
312
+ end
303
313
  microservice_count += 1
304
314
  end
305
315
  # Wait for the operator to wake up and remove the microservice processes
@@ -308,21 +318,49 @@ module OpenC3
308
318
  # Save TargetModel for last as it has the most to cleanup
309
319
  [InterfaceModel, RouterModel, ToolModel, WidgetModel, TargetModel].each do |model|
310
320
  model.find_all_by_plugin(plugin: @name, scope: @scope).each do |name, model_instance|
311
- model_instance.destroy
321
+ begin
322
+ model_instance.destroy
323
+ rescue Exception => error
324
+ errors << error
325
+ end
312
326
  end
313
327
  end
314
328
  # Cleanup Redis stuff that might have been left by microservices
315
329
  microservices.each do |name, model_instance|
316
- model_instance.cleanup
330
+ begin
331
+ model_instance.cleanup
332
+ rescue Exception => error
333
+ errors << error
334
+ end
335
+ end
336
+ # Raise all the errors at once
337
+ if errors.length > 0
338
+ message = ''
339
+ errors.each do |error|
340
+ message += "\n#{error.formatted}\n"
341
+ end
342
+ raise message
317
343
  end
318
344
  rescue Exception => error
319
- Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to #{error}")
345
+ Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to: #{error}")
346
+ ensure
347
+ # Double check everything is gone
348
+ found = []
349
+ [MicroserviceModel, InterfaceModel, RouterModel, ToolModel, WidgetModel, TargetModel].each do |model|
350
+ model.find_all_by_plugin(plugin: @name, scope: @scope).each do |name, model_instance|
351
+ found << model_instance
352
+ end
353
+ end
354
+ if found.length > 0
355
+ # If undeploy failed we need to not move forward with anything else
356
+ Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to: Plugin submodels still exist after undeploy = #{found.length}")
357
+ raise "Plugin #{@name} submodels still exist after undeploy = #{found.length}"
358
+ end
320
359
  end
321
360
 
322
361
  # Reinstall
323
362
  def restore
324
363
  plugin_hash = self.as_json(:allow_nan => true)
325
- plugin_hash['name'] = plugin_hash['name'].split("__")[0]
326
364
  OpenC3::PluginModel.install_phase2(plugin_hash, scope: @scope)
327
365
  @destroyed = false
328
366
  end