openc3 7.2.0 → 7.3.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +108 -29
  3. data/bin/pipinstall +5 -4
  4. data/bin/pipuninstall +5 -1
  5. data/bin/uvinstall +149 -0
  6. data/data/config/interface_modifiers.yaml +15 -0
  7. data/data/config/item_modifiers.yaml +2 -1
  8. data/data/config/microservice.yaml +5 -1
  9. data/data/config/screen.yaml +2 -1
  10. data/data/config/target.yaml +38 -0
  11. data/data/config/tool.yaml +3 -1
  12. data/data/config/widgets.yaml +43 -3
  13. data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
  14. data/data/xtce_schemas/xml.xsd +116 -0
  15. data/ext/openc3/ext/telemetry/telemetry.c +1 -1
  16. data/lib/openc3/accessors/binary_accessor.rb +4 -0
  17. data/lib/openc3/accessors/template_accessor.rb +2 -2
  18. data/lib/openc3/api/cmd_api.rb +5 -1
  19. data/lib/openc3/api/interface_api.rb +2 -2
  20. data/lib/openc3/api/limits_api.rb +89 -3
  21. data/lib/openc3/api/router_api.rb +3 -3
  22. data/lib/openc3/api/tlm_api.rb +1 -1
  23. data/lib/openc3/interfaces/http_client_interface.rb +1 -1
  24. data/lib/openc3/io/json_drb.rb +24 -8
  25. data/lib/openc3/io/json_rpc.rb +0 -2
  26. data/lib/openc3/logs/log_writer.rb +43 -7
  27. data/lib/openc3/logs/packet_log_writer.rb +1 -2
  28. data/lib/openc3/microservices/decom_common.rb +15 -4
  29. data/lib/openc3/microservices/decom_microservice.rb +6 -1
  30. data/lib/openc3/microservices/interface_microservice.rb +26 -12
  31. data/lib/openc3/microservices/queue_microservice.rb +4 -2
  32. data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
  33. data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
  34. data/lib/openc3/models/cvt_model.rb +7 -7
  35. data/lib/openc3/models/db_sharded_model.rb +2 -2
  36. data/lib/openc3/models/gem_model.rb +1 -1
  37. data/lib/openc3/models/interface_model.rb +17 -11
  38. data/lib/openc3/models/interface_status_model.rb +2 -2
  39. data/lib/openc3/models/metric_model.rb +2 -2
  40. data/lib/openc3/models/microservice_model.rb +21 -0
  41. data/lib/openc3/models/microservice_status_model.rb +2 -2
  42. data/lib/openc3/models/model.rb +2 -6
  43. data/lib/openc3/models/plugin_model.rb +204 -49
  44. data/lib/openc3/models/python_package_model.rb +210 -12
  45. data/lib/openc3/models/reingest_job_model.rb +1 -1
  46. data/lib/openc3/models/scope_model.rb +34 -30
  47. data/lib/openc3/models/script_status_model.rb +73 -38
  48. data/lib/openc3/models/target_model.rb +193 -6
  49. data/lib/openc3/models/tool_model.rb +33 -20
  50. data/lib/openc3/operators/microservice_operator.rb +33 -4
  51. data/lib/openc3/operators/operator.rb +15 -5
  52. data/lib/openc3/packets/commands.rb +7 -7
  53. data/lib/openc3/packets/limits.rb +33 -2
  54. data/lib/openc3/packets/packet.rb +6 -4
  55. data/lib/openc3/packets/packet_config.rb +5 -1
  56. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  57. data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
  58. data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
  59. data/lib/openc3/packets/structure.rb +16 -12
  60. data/lib/openc3/packets/telemetry.rb +3 -3
  61. data/lib/openc3/script/commands.rb +1 -1
  62. data/lib/openc3/script/extract.rb +20 -25
  63. data/lib/openc3/script/limits.rb +1 -1
  64. data/lib/openc3/script/suite_runner.rb +43 -9
  65. data/lib/openc3/script/web_socket_api.rb +45 -5
  66. data/lib/openc3/system/target.rb +4 -1
  67. data/lib/openc3/tools/table_manager/table_config.rb +4 -1
  68. data/lib/openc3/top_level.rb +1 -1
  69. data/lib/openc3/topics/command_decom_topic.rb +16 -1
  70. data/lib/openc3/topics/limits_event_topic.rb +63 -4
  71. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
  72. data/lib/openc3/utilities/aws_bucket.rb +10 -2
  73. data/lib/openc3/utilities/cli_generator.rb +5 -1
  74. data/lib/openc3/utilities/csv.rb +5 -5
  75. data/lib/openc3/utilities/env_helper.rb +13 -1
  76. data/lib/openc3/utilities/local_mode.rb +4 -1
  77. data/lib/openc3/utilities/pypi_url.rb +39 -0
  78. data/lib/openc3/utilities/reingest_job.rb +1 -1
  79. data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
  80. data/lib/openc3/utilities/running_script.rb +160 -100
  81. data/lib/openc3/utilities/script.rb +60 -13
  82. data/lib/openc3/utilities/store_autoload.rb +39 -1
  83. data/lib/openc3/utilities/store_queued.rb +5 -1
  84. data/lib/openc3/version.rb +5 -5
  85. data/lib/openc3.rb +6 -1
  86. data/templates/plugin/plugin.gemspec +1 -1
  87. data/templates/tool_angular/package.json +2 -2
  88. data/templates/tool_react/package.json +1 -1
  89. data/templates/tool_svelte/package.json +1 -1
  90. data/templates/tool_vue/package.json +3 -3
  91. data/templates/widget/package.json +2 -2
  92. metadata +7 -3
  93. data/templates/tool_vue/.nycrc +0 -3
  94. data/templates/widget/.nycrc +0 -3
@@ -195,7 +195,7 @@ module OpenC3
195
195
  msg_hash = critical_model.cmd_hash
196
196
  release_critical = true
197
197
  else
198
- next "Critical command #{msg_hash['release_critical']} not found"
198
+ next "Critical command '#{msg_hash['release_critical']}' not found (interface command processing)"
199
199
  end
200
200
  end
201
201
  if msg_hash.key?('target_control')
@@ -272,6 +272,11 @@ module OpenC3
272
272
  else
273
273
  next nil # Don't ack disabled targets
274
274
  end
275
+ rescue RangeError => e
276
+ # Command parameter out of range is a user error, not a bug,
277
+ # so only log the message and not the full stack trace
278
+ @logger.error "#{@interface.name}: #{e.message}"
279
+ next e.message
275
280
  rescue => e
276
281
  @logger.error "#{@interface.name}: #{msg_hash}"
277
282
  @logger.error "#{@interface.name}: #{e.formatted}"
@@ -281,6 +286,9 @@ module OpenC3
281
286
  command.extra ||= {}
282
287
  command.extra['cmd_string'] = msg_hash['cmd_string']
283
288
  command.extra['username'] = msg_hash['username']
289
+ command.extra['interface_name'] = @interface.name
290
+ # Original author of a queued command (set when released from a queue)
291
+ command.extra['queue_username'] = msg_hash['queue_username'] if msg_hash['queue_username']
284
292
  # Add approver info if this was a critical command that was approved
285
293
  if critical_model
286
294
  command.extra['approver'] = critical_model.approver
@@ -706,16 +714,19 @@ module OpenC3
706
714
  # Try to do clean disconnect because we're going down
707
715
  disconnect(false)
708
716
  end
709
- if @interface_or_router == 'INTERFACE'
710
- InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
711
- else
712
- RouterStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
717
+ unless @cancel_thread
718
+ if @interface_or_router == 'INTERFACE'
719
+ InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
720
+ else
721
+ RouterStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
722
+ end
713
723
  end
714
724
  @logger.info "#{@interface.name}: Stopped packet reading"
715
725
  end
716
726
 
717
727
  def handle_packet(packet)
718
- InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
728
+ # Skip status update if stop() has been called to avoid re-creating the status model
729
+ InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope) unless @cancel_thread
719
730
  packet.received_time = Time.now.sys unless packet.received_time
720
731
 
721
732
  if packet.stored
@@ -853,7 +864,8 @@ module OpenC3
853
864
 
854
865
  # If the interface is set to auto_reconnect then delay so the thread
855
866
  # can come back around and allow the interface a chance to reconnect.
856
- if allow_reconnect and @interface.auto_reconnect and @interface.state != 'DISCONNECTED'
867
+ # Skip reconnect if stop() has been called to avoid re-creating the status model
868
+ if allow_reconnect and @interface.auto_reconnect and @interface.state != 'DISCONNECTED' and !@cancel_thread
857
869
  attempting()
858
870
  if !@cancel_thread
859
871
  # @logger.debug "reconnect delay: #{@interface.reconnect_delay}"
@@ -861,10 +873,12 @@ module OpenC3
861
873
  end
862
874
  else
863
875
  @interface.state = 'DISCONNECTED'
864
- if @interface_or_router == 'INTERFACE'
865
- InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
866
- else
867
- RouterStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
876
+ unless @cancel_thread
877
+ if @interface_or_router == 'INTERFACE'
878
+ InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
879
+ else
880
+ RouterStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
881
+ end
868
882
  end
869
883
  end
870
884
  end
@@ -914,4 +928,4 @@ if __FILE__ == $0
914
928
  OpenC3::InterfaceMicroservice.run
915
929
  OpenC3::ThreadManager.instance.shutdown
916
930
  OpenC3::ThreadManager.instance.join
917
- end
931
+ end
@@ -76,12 +76,14 @@ module OpenC3
76
76
  end
77
77
  validate = command.key?('validate') ? command['validate'] : true
78
78
  timeout = command['timeout']
79
- cmd(command['target_name'], command['cmd_name'], cmd_params, queue: false, validate: validate, timeout: timeout, scope: @scope)
79
+ # Pass queue_username so Command History is attributed to the original
80
+ # author rather than the queue microservice name
81
+ cmd(command['target_name'], command['cmd_name'], cmd_params, queue: false, validate: validate, timeout: timeout, queue_username: command['username'], scope: @scope)
80
82
  elsif command['value']
81
83
  # Legacy format: use single string parameter for backwards compatibility
82
84
  validate = command.key?('validate') ? command['validate'] : true
83
85
  timeout = command['timeout']
84
- cmd(command['value'], queue: false, validate: validate, timeout: timeout, scope: @scope)
86
+ cmd(command['value'], queue: false, validate: validate, timeout: timeout, queue_username: command['username'], scope: @scope)
85
87
  else
86
88
  @logger.error "QueueProcessor: Invalid command format, missing required fields"
87
89
  end
@@ -0,0 +1,55 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2026 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is distributed in the hope that it will be useful,
7
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ # See LICENSE.md for more details.
10
+ #
11
+ # This file may also be used under the terms of a commercial license
12
+ # if purchased from OpenC3, Inc.
13
+
14
+ require 'time'
15
+ require 'openc3/utilities/migration'
16
+ require 'openc3/core_ext/time'
17
+ require 'openc3/models/scope_model'
18
+ require 'openc3/models/script_status_model'
19
+
20
+ module OpenC3
21
+ # Earlier versions of the Python script runner wrote the ScriptStatusModel
22
+ # updated_at field as an ISO8601 string (e.g. "2026-06-30T12:34:56.789012Z")
23
+ # instead of integer nanoseconds since the epoch. This migration rewrites any
24
+ # such string values as integers so updated_at is consistently an int.
25
+ #
26
+ # NOTE: The stored JSON is edited in place rather than reloaded through the
27
+ # model and re-created, because ScriptStatusModel#create always sets
28
+ # updated_at to the current time, which would discard the original timestamp.
29
+ class UpdatedAtToInt < Migration
30
+ def self.run
31
+ ScopeModel.names.each do |scope|
32
+ [ScriptStatusModel::RUNNING_PRIMARY_KEY, ScriptStatusModel::COMPLETED_PRIMARY_KEY].each do |key|
33
+ primary_key = "#{key}__#{scope}"
34
+ Store.hgetall(primary_key).each do |name, json|
35
+ data = JSON.parse(json, allow_nan: true)
36
+ updated_at = data['updated_at']
37
+ next unless updated_at.is_a?(String)
38
+
39
+ begin
40
+ data['updated_at'] = Time.parse(updated_at).to_nsec_from_epoch
41
+ rescue ArgumentError
42
+ # Leave unparsable values untouched
43
+ next
44
+ end
45
+ Store.hset(primary_key, name, JSON.generate(data, allow_nan: true))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ unless ENV['OPENC3_NO_MIGRATE']
54
+ OpenC3::UpdatedAtToInt.run
55
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'openc3/utilities/local_mode'
3
+ require 'openc3/utilities/migration'
4
+ require 'openc3/models/scope_model'
5
+ require 'openc3/models/setting_model'
6
+
7
+ module OpenC3
8
+ # The system_health setting was originally stored as a Ruby Hash. Local mode
9
+ # writes settings to disk with File.write and reads them back as a String, so
10
+ # the Hash round tripped into a Ruby inspect String which nothing can parse.
11
+ # Store it as a JSON string like every other setting and repair existing data.
12
+ class SystemHealthJson < Migration
13
+ def self.run
14
+ setting = SettingModel.get(name: 'system_health')
15
+ data = repair(setting && setting['data'])
16
+ SettingModel.set({ name: 'system_health', data: JSON.generate(data) }, scope: 'DEFAULT')
17
+ # Repair the local mode file as well or sync_settings puts the bad data back
18
+ LocalMode.save_setting('DEFAULT', 'system_health', JSON.generate(data))
19
+ end
20
+
21
+ # @return [Hash] the existing settings if they can be recovered, otherwise defaults
22
+ def self.repair(data)
23
+ return ScopeModel::SYSTEM_HEALTH_DEFAULTS if data.nil?
24
+ return data if Hash === data
25
+ return ScopeModel::SYSTEM_HEALTH_DEFAULTS unless String === data
26
+ begin
27
+ parsed = JSON.parse(data)
28
+ rescue JSON::ParserError
29
+ # Ruby inspect format, e.g. {"cpu" => {"redThreshold" => 90, ... => nil}}
30
+ # Ruby 3.3 and earlier had no spaces around the rocket. All the values are
31
+ # numbers, booleans, or nil so there's nothing to accidentally rewrite.
32
+ parsed = JSON.parse(data.gsub(/\s*=>\s*/, ': ').gsub(/\bnil\b/, 'null')) rescue nil
33
+ end
34
+ Hash === parsed ? parsed : ScopeModel::SYSTEM_HEALTH_DEFAULTS
35
+ end
36
+ end
37
+ end
38
+
39
+ unless ENV['OPENC3_NO_MIGRATE']
40
+ OpenC3::SystemHealthJson.run
41
+ end
@@ -26,8 +26,8 @@ module OpenC3
26
26
  @@override_cache = {}
27
27
 
28
28
  VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED]
29
- def self.build_json_from_packet(packet)
30
- packet.decom
29
+ def self.build_json_from_packet(packet, include_limits_states: true)
30
+ packet.decom(include_limits_states: include_limits_states)
31
31
  end
32
32
 
33
33
  # Get a Store instance routed to the correct db_shard for a target
@@ -86,7 +86,7 @@ module OpenC3
86
86
  return hash if hash and (now - cache_time) < cache_timeout
87
87
  end
88
88
  packet = store_for_target(target_name, scope: scope).hget(key, packet_name)
89
- raise "Packet '#{target_name} #{packet_name}' does not exist" unless packet
89
+ raise "Packet '#{target_name} #{packet_name}' has no current values in CVT" unless packet
90
90
  hash = JSON.parse(packet, allow_nan: true, create_additions: true)
91
91
  @@packet_cache[tgt_pkt_key] = [now, hash]
92
92
  hash
@@ -107,7 +107,7 @@ module OpenC3
107
107
  hash["#{item_name}__C"] = value
108
108
  hash[item_name] = value
109
109
  else
110
- raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
110
+ raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (set_item)"
111
111
  end
112
112
  set(hash, target_name: target_name, packet_name: packet_name, queued: queued, scope: scope)
113
113
  end
@@ -252,7 +252,7 @@ module OpenC3
252
252
  when :FORMATTED, :WITH_UNITS
253
253
  hash["#{item_name}__F"] = value.to_s # Always a String
254
254
  else
255
- raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
255
+ raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (override)"
256
256
  end
257
257
 
258
258
  tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
@@ -277,7 +277,7 @@ module OpenC3
277
277
  when :FORMATTED, :WITH_UNITS
278
278
  hash.delete("#{item_name}__F")
279
279
  else
280
- raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
280
+ raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (normalize)"
281
281
  end
282
282
 
283
283
  tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
@@ -327,7 +327,7 @@ module OpenC3
327
327
  when :RAW
328
328
  types = [item_name]
329
329
  else
330
- raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name}"
330
+ raise "Unknown type '#{type}' for #{target_name} #{packet_name} #{item_name} (get_item)"
331
331
  end
332
332
 
333
333
  tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
@@ -79,7 +79,7 @@ module OpenC3
79
79
  end
80
80
 
81
81
  # DB_Shard-aware create: writes to the store instance for the given db_shard.
82
- def _db_sharded_create(db_shard, update: false, force: false, queued: false, isoformat: false, expire_seconds: nil)
82
+ def _db_sharded_create(db_shard, update: false, force: false, queued: false, expire_seconds: nil)
83
83
  db_shard_store = self.class.store.instance(db_shard: db_shard)
84
84
  unless force
85
85
  existing = db_shard_store.hget(@primary_key, @name)
@@ -89,7 +89,7 @@ module OpenC3
89
89
  raise RuntimeError.new("#{@primary_key}:#{@name} doesn't exist at update") if update
90
90
  end
91
91
  end
92
- @updated_at = isoformat ? Time.now.utc.iso8601 : Time.now.utc.to_nsec_from_epoch
92
+ @updated_at = Time.now.utc.to_nsec_from_epoch
93
93
 
94
94
  if queued
95
95
  store = self.class.store_queued.instance(db_shard: db_shard)
@@ -47,7 +47,7 @@ module OpenC3
47
47
  return path if File.exist?(path)
48
48
  path = "#{ENV['GEM_HOME']}/cache/#{name}"
49
49
  return path if File.exist?(path)
50
- raise "Gem #{name} not found"
50
+ raise "Gem '#{name}' not found"
51
51
  end
52
52
 
53
53
  def self.put(gem_file_path, gem_install: true, scope:)
@@ -256,7 +256,7 @@ module OpenC3
256
256
 
257
257
  def ensure_target_exists(target_name)
258
258
  target = TargetModel.get(name: target_name, scope: @scope)
259
- raise "Target #{target_name} does not exist" unless target
259
+ raise "Target '#{target_name}' does not exist (InterfaceModel)" unless target
260
260
  target
261
261
  end
262
262
 
@@ -444,9 +444,11 @@ module OpenC3
444
444
  microservice
445
445
  end
446
446
 
447
- # Looks up the deployed MicroserviceModel and destroy the microservice model
448
- # should should trigger the operator to kill the microservice that in turn
449
- # will destroy the InterfaceStatusModel when a stop is called.
447
+ # Destroys the deployed MicroserviceModel which triggers the operator to kill
448
+ # the microservice. Also directly destroys the Interface/RouterStatusModel in
449
+ # an ensure block. The microservice stop() no longer recreates the status
450
+ # model once cancel_thread is set, so undeploy must clean it up itself to
451
+ # avoid leaving an orphaned status model behind.
450
452
  def undeploy
451
453
  type = self.class._get_type
452
454
  name = "#{@scope}__#{type}__#{@name}"
@@ -455,15 +457,19 @@ module OpenC3
455
457
  model.destroy
456
458
  ConfigTopic.write({ kind: 'deleted', type: type.downcase, name: @name, plugin: @plugin }, scope: @scope)
457
459
  end
458
-
459
- if type == 'INTERFACE'
460
- status_model = InterfaceStatusModel.get_model(name: @name, scope: @scope)
461
- else
462
- status_model = RouterStatusModel.get_model(name: @name, scope: @scope)
463
- end
464
- status_model.destroy if status_model
465
460
  rescue Exception => error
466
461
  Logger.error("Error undeploying interface/router model #{@name} in scope #{@scope} due to #{error}")
462
+ ensure
463
+ begin
464
+ if type == 'INTERFACE'
465
+ status_model = InterfaceStatusModel.get_model(name: @name, scope: @scope)
466
+ else
467
+ status_model = RouterStatusModel.get_model(name: @name, scope: @scope)
468
+ end
469
+ status_model.destroy if status_model
470
+ rescue Exception => error
471
+ Logger.error("Error destroying #{type&.downcase || 'unknown'} status model #{@name} in scope #{@scope} due to #{error}")
472
+ end
467
473
  end
468
474
 
469
475
  def unmap_target(target_name, cmd_only: false, tlm_only: false)
@@ -118,8 +118,8 @@ module OpenC3
118
118
  @rxcnt = rxcnt
119
119
  end
120
120
 
121
- def create(update: false, force: false, queued: false, isoformat: false)
122
- _db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued, isoformat: isoformat)
121
+ def create(update: false, force: false, queued: false)
122
+ _db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued)
123
123
  end
124
124
 
125
125
  def destroy
@@ -77,8 +77,8 @@ module OpenC3
77
77
  @db_shard = db_shard.to_i
78
78
  end
79
79
 
80
- def create(update: false, force: false, queued: false, isoformat: false, expire_seconds: nil)
81
- _db_sharded_create(@db_shard, update: update, force: force, queued: queued, isoformat: isoformat, expire_seconds: expire_seconds)
80
+ def create(update: false, force: false, queued: false, expire_seconds: nil)
81
+ _db_sharded_create(@db_shard, update: update, force: force, queued: queued, expire_seconds: expire_seconds)
82
82
  end
83
83
 
84
84
  def destroy
@@ -140,6 +140,27 @@ module OpenC3
140
140
  @bucket = Bucket.getClient()
141
141
  end
142
142
 
143
+ # Resolve the Python environment variables that will be used at runtime.
144
+ # This mirrors the logic in MicroserviceOperator#convert_microservice_to_process_definition.
145
+ def runtime_python_env
146
+ python_bin = ENV['OPENC3_PYTHON_BIN'] || '/openc3/python/.venv/bin/python'
147
+ result = { 'OPENC3_PYTHON_BIN' => python_bin }
148
+ if @needs_dependencies && @plugin
149
+ sanitized_name = "#{@scope}__#{@plugin}".tr('^a-zA-Z0-9_-', '_')
150
+ candidate = "/gems/plugin_venvs/#{sanitized_name}/.venv"
151
+ if File.directory?(candidate)
152
+ result['VIRTUAL_ENV'] = candidate
153
+ result['PYTHONUSERBASE'] = candidate
154
+ site_packages = Dir.glob("#{candidate}/lib/python*/site-packages").first
155
+ result['PYTHONPATH'] = site_packages
156
+ else
157
+ result['VIRTUAL_ENV'] = nil
158
+ result['PYTHONUSERBASE'] = '/gems/python_packages'
159
+ end
160
+ end
161
+ result
162
+ end
163
+
143
164
  def as_json(*a)
144
165
  {
145
166
  'name' => @name,
@@ -76,8 +76,8 @@ module OpenC3
76
76
  @custom = custom
77
77
  end
78
78
 
79
- def create(update: false, force: false, queued: false, isoformat: false)
80
- _db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued, isoformat: isoformat)
79
+ def create(update: false, force: false, queued: false)
80
+ _db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued)
81
81
  end
82
82
 
83
83
  def destroy
@@ -134,7 +134,7 @@ module OpenC3
134
134
 
135
135
  # Update the Redis hash at primary_key and set the field "name"
136
136
  # to the JSON generated via calling as_json
137
- def create(update: false, force: false, queued: false, isoformat: false)
137
+ def create(update: false, force: false, queued: false)
138
138
  unless force
139
139
  existing = self.class.store.hget(@primary_key, @name)
140
140
  if existing
@@ -143,11 +143,7 @@ module OpenC3
143
143
  raise "#{@primary_key}:#{@name} doesn't exist at update" if update
144
144
  end
145
145
  end
146
- if isoformat
147
- @updated_at = Time.now.utc.iso8601
148
- else
149
- @updated_at = Time.now.utc.to_nsec_from_epoch
150
- end
146
+ @updated_at = Time.now.utc.to_nsec_from_epoch
151
147
 
152
148
  if queued
153
149
  write_store = self.class.store_queued