openc3 7.4.1 → 7.5.1
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.
- checksums.yaml +4 -4
- data/bin/pipinstall +18 -5
- data/bin/pipuninstall +1 -1
- data/bin/uvinstall +36 -8
- data/data/config/default_plugins.yaml +43 -0
- data/data/config/interface_modifiers.yaml +22 -7
- data/data/config/item_modifiers.yaml +14 -4
- data/data/config/plugins.yaml +1 -1
- data/data/config/widgets.yaml +1 -1
- data/lib/openc3/api/cmd_api.rb +10 -13
- data/lib/openc3/api/interface_api.rb +1 -2
- data/lib/openc3/api/limits_api.rb +149 -160
- data/lib/openc3/api/metrics_api.rb +26 -1
- data/lib/openc3/api/router_api.rb +1 -2
- data/lib/openc3/api/tlm_api.rb +225 -71
- data/lib/openc3/config/config_parser.rb +3 -1
- data/lib/openc3/core_ext/tempfile.rb +2 -0
- data/lib/openc3/interfaces/file_interface.rb +6 -1
- data/lib/openc3/interfaces/http_client_interface.rb +14 -6
- data/lib/openc3/interfaces/interface.rb +27 -3
- data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
- data/lib/openc3/interfaces/stream_interface.rb +22 -3
- data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
- data/lib/openc3/interfaces/udp_interface.rb +16 -2
- data/lib/openc3/interfaces.rb +1 -0
- data/lib/openc3/logs/log_writer.rb +14 -4
- data/lib/openc3/microservices/decom_microservice.rb +30 -5
- data/lib/openc3/microservices/interface_decom_common.rb +3 -1
- data/lib/openc3/microservices/interface_microservice.rb +146 -50
- data/lib/openc3/microservices/log_microservice.rb +1 -3
- data/lib/openc3/microservices/microservice.rb +25 -1
- data/lib/openc3/microservices/plugin_microservice.rb +32 -0
- data/lib/openc3/microservices/text_log_microservice.rb +1 -3
- data/lib/openc3/models/cvt_model.rb +7 -2
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/interface_status_model.rb +6 -0
- data/lib/openc3/models/metric_model.rb +52 -4
- data/lib/openc3/models/microservice_model.rb +7 -3
- data/lib/openc3/models/plugin_model.rb +12 -12
- data/lib/openc3/models/python_package_model.rb +3 -13
- data/lib/openc3/models/target_model.rb +92 -15
- data/lib/openc3/operators/microservice_operator.rb +5 -0
- data/lib/openc3/operators/operator.rb +69 -0
- data/lib/openc3/packets/packet.rb +10 -4
- data/lib/openc3/packets/packet_config.rb +12 -1
- data/lib/openc3/packets/packet_item.rb +40 -22
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/structure_item.rb +3 -2
- data/lib/openc3/script/api_shared.rb +62 -86
- data/lib/openc3/script/extract.rb +263 -20
- data/lib/openc3/script/plugins.rb +4 -11
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/script/storage.rb +81 -10
- data/lib/openc3/system/system.rb +9 -6
- data/lib/openc3/topics/command_decom_topic.rb +1 -1
- data/lib/openc3/topics/command_topic.rb +2 -1
- data/lib/openc3/topics/limits_event_topic.rb +90 -3
- data/lib/openc3/topics/router_topic.rb +7 -2
- data/lib/openc3/topics/system_events_topic.rb +9 -6
- data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
- data/lib/openc3/topics/telemetry_topic.rb +3 -2
- data/lib/openc3/topics/topic.rb +162 -3
- data/lib/openc3/utilities/bucket.rb +5 -0
- data/lib/openc3/utilities/cli_generator.rb +67 -15
- data/lib/openc3/utilities/config_overlay.rb +35 -17
- data/lib/openc3/utilities/default_plugins.rb +33 -0
- data/lib/openc3/utilities/local_mode.rb +82 -46
- data/lib/openc3/utilities/metric.rb +16 -0
- data/lib/openc3/utilities/process_stats.rb +160 -0
- data/lib/openc3/utilities/pypi_url.rb +65 -0
- data/lib/openc3/utilities/questdb_client.rb +140 -32
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/secrets.rb +5 -0
- data/lib/openc3/utilities/store_autoload.rb +23 -4
- data/lib/openc3/utilities/store_queued.rb +37 -9
- data/lib/openc3/utilities/target_file.rb +57 -12
- data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
- data/lib/openc3/version.rb +5 -5
- data/templates/conversion/conversion.py +6 -2
- data/templates/conversion/conversion.rb +5 -0
- data/templates/plugin/.github/workflows/playwright.yml +62 -0
- data/templates/plugin/.github/workflows/release.yml +45 -0
- data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
- data/templates/plugin/.gitignore +6 -0
- data/templates/plugin/README.md +114 -2
- data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
- data/templates/plugin/plugin.gemspec +10 -1
- data/templates/plugin/pyproject.toml +119 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_angular/pnpm-workspace.yaml +16 -0
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_react/pnpm-workspace.yaml +16 -0
- data/templates/tool_svelte/package.json +2 -1
- data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
- data/templates/tool_vue/package.json +3 -3
- data/templates/tool_vue/pnpm-workspace.yaml +16 -0
- data/templates/widget/package.json +2 -2
- data/templates/widget/pnpm-workspace.yaml +16 -0
- metadata +16 -1
|
@@ -308,9 +308,13 @@ module OpenC3
|
|
|
308
308
|
end
|
|
309
309
|
|
|
310
310
|
def cleanup
|
|
311
|
-
# Cleanup metrics
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
# Cleanup metrics - both the microservice's own and the process metrics
|
|
312
|
+
# the operator reported for it
|
|
313
|
+
# The MicroserviceModel has already been deleted during uninstall, so
|
|
314
|
+
# shard lookup by name can no longer find its configured database.
|
|
315
|
+
[MetricModel::PRIMARY_KEY, MetricModel::PROCESS_PRIMARY_KEY].each do |key|
|
|
316
|
+
MetricModel.new(name: @name, scope: @scope, db_shard: @db_shard, primary_key: key).destroy
|
|
317
|
+
end
|
|
314
318
|
end
|
|
315
319
|
end
|
|
316
320
|
end
|
|
@@ -278,7 +278,10 @@ module OpenC3
|
|
|
278
278
|
|
|
279
279
|
if File.exist?(pyproject_path) || File.exist?(requirements_path)
|
|
280
280
|
pypi_url = resolve_pypi_url(scope: scope)
|
|
281
|
-
|
|
281
|
+
# Skipped for any dry run: diff_only runs inline in an HTTP request, so
|
|
282
|
+
# a slow or unreachable PyPI would time it out, and it must not mutate
|
|
283
|
+
# the plugin venv.
|
|
284
|
+
unless dry_run
|
|
282
285
|
pypi_args = build_pypi_args(pypi_url)
|
|
283
286
|
|
|
284
287
|
# Install Python dependencies into an isolated per-plugin venv when UV
|
|
@@ -428,11 +431,9 @@ module OpenC3
|
|
|
428
431
|
# Dry run: which modified files would this plugin's install supersede?
|
|
429
432
|
# Returns a list of "TARGET/path" names whose live (modified) content
|
|
430
433
|
# differs from the rendered plugin content. Read-only; no side effects.
|
|
434
|
+
# Errors propagate so callers can tell a failed check from an empty diff.
|
|
431
435
|
def self.modified_diff(plugin_hash, scope:)
|
|
432
436
|
install_phase2(plugin_hash, scope: scope, diff_only: true)
|
|
433
|
-
rescue => e
|
|
434
|
-
Logger.warn("PluginModel.modified_diff failed: #{e.message}")
|
|
435
|
-
[]
|
|
436
437
|
end
|
|
437
438
|
|
|
438
439
|
def initialize(
|
|
@@ -597,25 +598,24 @@ module OpenC3
|
|
|
597
598
|
def self.resolve_pypi_url(scope:)
|
|
598
599
|
pypi_url = nil
|
|
599
600
|
begin
|
|
600
|
-
pypi_url = get_setting('pypi_url', scope: scope)
|
|
601
|
-
pypi_url += '/simple' if pypi_url
|
|
601
|
+
pypi_url = PypiUrl.index_url(get_setting('pypi_url', scope: scope))
|
|
602
602
|
rescue => e
|
|
603
603
|
Logger.error("Failed to retrieve pypi_url: #{e.formatted}")
|
|
604
604
|
ensure
|
|
605
605
|
if pypi_url.nil?
|
|
606
|
-
pypi_url = ENV.fetch('PYPI_URL', nil)
|
|
607
|
-
pypi_url += '/simple' if pypi_url
|
|
606
|
+
pypi_url = PypiUrl.index_url(ENV.fetch('PYPI_URL', nil))
|
|
608
607
|
pypi_url ||= PypiUrl::DEFAULT
|
|
609
608
|
end
|
|
610
609
|
end
|
|
611
610
|
PypiUrl.validate(pypi_url)
|
|
612
611
|
end
|
|
613
612
|
|
|
614
|
-
# Build the argv array
|
|
613
|
+
# Build the argv array of pypi index arguments. Kept as a delegate so
|
|
614
|
+
# callers that already reference PluginModel keep working; the logic lives
|
|
615
|
+
# in PypiUrl because PythonPackageModel needs it too and must not have to
|
|
616
|
+
# require this file.
|
|
615
617
|
def self.build_pypi_args(pypi_url)
|
|
616
|
-
|
|
617
|
-
args += ["--trusted-host", URI.parse(pypi_url).host] unless ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
618
|
-
args
|
|
618
|
+
PypiUrl.build_args(pypi_url)
|
|
619
619
|
end
|
|
620
620
|
|
|
621
621
|
# Build a sanitized venv directory name from scope and plugin name.
|
|
@@ -235,29 +235,19 @@ module OpenC3
|
|
|
235
235
|
end
|
|
236
236
|
package_filename = File.basename(package_file_path)
|
|
237
237
|
begin
|
|
238
|
-
pypi_url = get_setting('pypi_url', scope: scope)
|
|
239
|
-
if pypi_url
|
|
240
|
-
pypi_url += '/simple'
|
|
241
|
-
end
|
|
238
|
+
pypi_url = PypiUrl.index_url(get_setting('pypi_url', scope: scope))
|
|
242
239
|
rescue => e
|
|
243
240
|
Logger.error("Failed to retrieve pypi_url: #{e.formatted}")
|
|
244
241
|
ensure
|
|
245
242
|
if pypi_url.nil?
|
|
246
243
|
# If Redis isn't running try the ENV, then simply pypi.org/simple
|
|
247
|
-
pypi_url = ENV.fetch('PYPI_URL', nil)
|
|
248
|
-
if pypi_url
|
|
249
|
-
pypi_url += '/simple'
|
|
250
|
-
end
|
|
244
|
+
pypi_url = PypiUrl.index_url(ENV.fetch('PYPI_URL', nil))
|
|
251
245
|
pypi_url ||= PypiUrl::DEFAULT
|
|
252
246
|
end
|
|
253
247
|
end
|
|
254
248
|
pypi_url = PypiUrl.validate(pypi_url)
|
|
255
249
|
Logger.info "Installing python package: #{name_or_path}"
|
|
256
|
-
|
|
257
|
-
pip_args = ["-i", pypi_url, package_file_path]
|
|
258
|
-
else
|
|
259
|
-
pip_args = ["-i", pypi_url, "--trusted-host", URI.parse(pypi_url).host, package_file_path]
|
|
260
|
-
end
|
|
250
|
+
pip_args = PypiUrl.build_args(pypi_url) + [package_file_path]
|
|
261
251
|
spawn_env = {}
|
|
262
252
|
if plugin
|
|
263
253
|
venv_path = "#{PLUGIN_VENVS_DIR}/#{plugin}/.venv"
|
|
@@ -109,10 +109,14 @@ module OpenC3
|
|
|
109
109
|
modified_targets = []
|
|
110
110
|
if ENV['OPENC3_LOCAL_MODE']
|
|
111
111
|
modified_targets += OpenC3::LocalMode.modified_targets(scope: scope)
|
|
112
|
+
elsif !OpenC3::LocalMode.local_only_targets.empty?
|
|
113
|
+
modified_targets += OpenC3::LocalMode.modified_targets(scope: scope).select { |name| OpenC3::LocalMode.local_only_target?(name) }
|
|
112
114
|
end
|
|
113
115
|
# Always list the bucket, even in local mode, since the local directory
|
|
114
|
-
# only holds what COSMOS itself wrote there
|
|
115
|
-
|
|
116
|
+
# only holds what COSMOS itself wrote there. Local only targets are
|
|
117
|
+
# never read from the bucket.
|
|
118
|
+
bucket_targets = Bucket.getClient().list_files(bucket: ENV['OPENC3_CONFIG_BUCKET'], path: "#{scope}/targets_modified/", only_directories: true)
|
|
119
|
+
modified_targets += bucket_targets.reject { |name| OpenC3::LocalMode.local_only_target?(name) }
|
|
116
120
|
modified_targets.each do |target_name|
|
|
117
121
|
# A target could have been deleted without removing the modified files
|
|
118
122
|
# Thus we have to check for the existence of the target_name key
|
|
@@ -186,13 +190,15 @@ module OpenC3
|
|
|
186
190
|
def self.modified_files(target_name, scope:)
|
|
187
191
|
modified = []
|
|
188
192
|
|
|
189
|
-
if ENV['OPENC3_LOCAL_MODE']
|
|
193
|
+
if ENV['OPENC3_LOCAL_MODE'] or OpenC3::LocalMode.local_only_target?(target_name)
|
|
190
194
|
# LocalMode reports target relative paths, i.e. "procedures/new.rb",
|
|
191
195
|
# so add the target name to match the bucket listing below
|
|
192
196
|
modified += OpenC3::LocalMode.modified_files(target_name, scope: scope).map do |file_path|
|
|
193
197
|
"#{target_name}/#{file_path}"
|
|
194
198
|
end
|
|
195
199
|
end
|
|
200
|
+
# Local only targets are never read from the bucket
|
|
201
|
+
return modified.uniq.sort if OpenC3::LocalMode.local_only_target?(target_name)
|
|
196
202
|
# Always list the bucket, even in local mode. The local directory only
|
|
197
203
|
# holds what COSMOS itself wrote there, so a file placed directly in the
|
|
198
204
|
# bucket is modified but has no local copy.
|
|
@@ -216,16 +222,28 @@ module OpenC3
|
|
|
216
222
|
# some modified files while keeping others); when nil, every modified file
|
|
217
223
|
# for the target is removed (the original behavior).
|
|
218
224
|
def self.delete_modified(target_name, scope:, files: nil)
|
|
225
|
+
# Validate target_name to not allow directory traversal
|
|
226
|
+
unless OpenC3::LocalMode.safe_key?(target_name) and !target_name.include?('/')
|
|
227
|
+
raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
|
|
228
|
+
end
|
|
219
229
|
if files && !files.empty?
|
|
230
|
+
# Only delete names that are actually modified files of this target.
|
|
231
|
+
# Anything else (another target, another scope via '..') is ignored.
|
|
232
|
+
allowed = modified_files(target_name, scope: scope)
|
|
220
233
|
files.each do |name|
|
|
234
|
+
next unless String === name
|
|
235
|
+
name = OpenC3::TargetFile.strip_modified(name)
|
|
236
|
+
next unless allowed.include?(name)
|
|
221
237
|
# TargetFile.destroy handles both local mode and the bucket.
|
|
222
238
|
OpenC3::TargetFile.destroy(scope, name)
|
|
223
239
|
end
|
|
224
240
|
return
|
|
225
241
|
end
|
|
226
|
-
if ENV['OPENC3_LOCAL_MODE']
|
|
242
|
+
if ENV['OPENC3_LOCAL_MODE'] or OpenC3::LocalMode.local_only_target?(target_name)
|
|
227
243
|
OpenC3::LocalMode.delete_modified(target_name, scope: scope)
|
|
228
244
|
end
|
|
245
|
+
# Local only targets are never written to the bucket
|
|
246
|
+
return if OpenC3::LocalMode.local_only_target?(target_name)
|
|
229
247
|
bucket = Bucket.getClient()
|
|
230
248
|
# Delete the remote files as well
|
|
231
249
|
resp = bucket.list_objects(
|
|
@@ -249,7 +267,7 @@ module OpenC3
|
|
|
249
267
|
Zip.continue_on_exists_proc = true
|
|
250
268
|
zip = Zip::File.open(zip_filename, create: true)
|
|
251
269
|
|
|
252
|
-
if ENV['OPENC3_LOCAL_MODE']
|
|
270
|
+
if ENV['OPENC3_LOCAL_MODE'] or OpenC3::LocalMode.local_only_target?(target_name)
|
|
253
271
|
OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
|
|
254
272
|
else
|
|
255
273
|
bucket = Bucket.getClient()
|
|
@@ -332,6 +350,35 @@ module OpenC3
|
|
|
332
350
|
result
|
|
333
351
|
end
|
|
334
352
|
|
|
353
|
+
# Returns just the packet names without fetching and parsing every packet
|
|
354
|
+
# definition, which is expensive for targets with thousands of packets.
|
|
355
|
+
# @return [Array<String>] Sorted packet names under the target_name
|
|
356
|
+
def self.packet_names(target_name, type: :TLM, scope:)
|
|
357
|
+
raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
|
|
358
|
+
raise "Target '#{target_name}' does not exist for scope: #{scope} (TargetModel)" unless get(name: target_name, scope: scope)
|
|
359
|
+
|
|
360
|
+
store_for_target(target_name, scope: scope).hkeys("#{scope}__openc3#{type.to_s.downcase}__#{target_name}").sort
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Returns the names of the hidden packets without parsing every packet definition.
|
|
364
|
+
# The list is stored when the target is installed. Targets installed before the
|
|
365
|
+
# list existed have it built once from the packet definitions and stored.
|
|
366
|
+
# @return [Array<String>] Sorted hidden packet names under the target_name
|
|
367
|
+
def self.hidden_packet_names(target_name, type: :TLM, scope:)
|
|
368
|
+
raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
|
|
369
|
+
|
|
370
|
+
store = store_for_target(target_name, scope: scope)
|
|
371
|
+
# Stored as a JSON string rather than a set so an empty list is distinct from a missing one
|
|
372
|
+
# e.g. DEFAULT__openc3tlm__INST__hidden
|
|
373
|
+
key = "#{scope}__openc3#{type.to_s.downcase}__#{target_name}__hidden"
|
|
374
|
+
names_json = store.get(key)
|
|
375
|
+
return JSON.parse(names_json) if names_json
|
|
376
|
+
|
|
377
|
+
names = packets(target_name, type: type, scope: scope).select { |packet| packet['hidden'] }.map { |packet| packet['packet_name'] }
|
|
378
|
+
store.set(key, JSON.generate(names))
|
|
379
|
+
names
|
|
380
|
+
end
|
|
381
|
+
|
|
335
382
|
# @return [Array<Hash>] All packet hashes under the target_name
|
|
336
383
|
def self.all_packet_name_descriptions(target_name, type: :TLM, scope:)
|
|
337
384
|
self.packets(target_name, type: type, scope: scope).map! { |hash| hash.slice("packet_name", "description") }
|
|
@@ -411,10 +458,13 @@ module OpenC3
|
|
|
411
458
|
end
|
|
412
459
|
|
|
413
460
|
def self.get_item_to_packet_map(target_name, scope:)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
item_map_key = "#{scope}__#{target_name}__item_to_packet_map"
|
|
461
|
+
# Normalize before building the key so lowercase lookups share the entry
|
|
462
|
+
# that update_store_item_map and undeploy refresh and invalidate
|
|
417
463
|
target_name = target_name.upcase
|
|
464
|
+
# The cache is keyed by item_map_key since the same target can exist in multiple scopes
|
|
465
|
+
item_map_key = "#{scope}__#{target_name}__item_to_packet_map"
|
|
466
|
+
cache_time, item_map = @@item_map_cache[item_map_key]
|
|
467
|
+
return item_map if item_map and (Time.now - cache_time) < ITEM_MAP_CACHE_TIMEOUT
|
|
418
468
|
store = store_for_target(target_name, scope: scope)
|
|
419
469
|
json_data = store.get(item_map_key)
|
|
420
470
|
if json_data
|
|
@@ -423,7 +473,7 @@ module OpenC3
|
|
|
423
473
|
item_map = build_item_to_packet_map(target_name, scope: scope)
|
|
424
474
|
store.set(item_map_key, JSON.generate(item_map, allow_nan: true))
|
|
425
475
|
end
|
|
426
|
-
@@item_map_cache[
|
|
476
|
+
@@item_map_cache[item_map_key] = [Time.now, item_map]
|
|
427
477
|
return item_map
|
|
428
478
|
end
|
|
429
479
|
|
|
@@ -751,11 +801,12 @@ module OpenC3
|
|
|
751
801
|
# Version History is an Enterprise feature. The plugin deploy runs in the
|
|
752
802
|
# cmd-tlm-api process, which (unlike script-runner-api) doesn't otherwise
|
|
753
803
|
# require the store, so lazily load it on demand. Returns false in Core
|
|
754
|
-
# builds where the Enterprise gem isn't present
|
|
804
|
+
# builds where the Enterprise gem isn't present, and when the store is
|
|
805
|
+
# disabled (OPENC3_VERSION_HISTORY_DIR unset): its commits no-op there, so
|
|
806
|
+
# versioning would drop the modified copy without preserving it.
|
|
755
807
|
def version_store_available?
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
defined?(::VersionStore) ? true : false
|
|
808
|
+
require 'openc3-enterprise/utilities/version_store' unless defined?(::VersionStore)
|
|
809
|
+
defined?(::VersionStore) && ::VersionStore.enabled? ? true : false
|
|
759
810
|
rescue LoadError
|
|
760
811
|
false
|
|
761
812
|
end
|
|
@@ -765,6 +816,8 @@ module OpenC3
|
|
|
765
816
|
# copy), commits it, removes it, then commits the incoming plugin content.
|
|
766
817
|
# Best-effort: a versioning failure must not abort the plugin upgrade.
|
|
767
818
|
def apply_upgrade_version(name, new_data, ctx)
|
|
819
|
+
# Local only targets have no modified copy in the bucket
|
|
820
|
+
return if OpenC3::LocalMode.local_only_name?(name)
|
|
768
821
|
modified_key = "#{@scope}/targets_modified/#{name}"
|
|
769
822
|
resp = @bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: modified_key)
|
|
770
823
|
return unless resp && resp.body
|
|
@@ -791,6 +844,7 @@ module OpenC3
|
|
|
791
844
|
# a modified copy exists and differs (by bytes) from the rendered plugin
|
|
792
845
|
# content. Read-only.
|
|
793
846
|
def collect_modified_diff(name, new_data, collector)
|
|
847
|
+
return if OpenC3::LocalMode.local_only_name?(name)
|
|
794
848
|
resp = @bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: "#{@scope}/targets_modified/#{name}")
|
|
795
849
|
return unless resp && resp.body
|
|
796
850
|
collector << name if resp.body.read.b != new_data.b
|
|
@@ -843,6 +897,8 @@ module OpenC3
|
|
|
843
897
|
db_shard_store = Store.instance(db_shard: @db_shard)
|
|
844
898
|
db_shard_store.del("#{@scope}__openc3tlm__#{@name}")
|
|
845
899
|
db_shard_store.del("#{@scope}__openc3cmd__#{@name}")
|
|
900
|
+
db_shard_store.del("#{@scope}__openc3tlm__#{@name}__hidden")
|
|
901
|
+
db_shard_store.del("#{@scope}__openc3cmd__#{@name}__hidden")
|
|
846
902
|
db_shard_store.del("#{@scope}__TELEMETRYCNTS__{#{@name}}")
|
|
847
903
|
db_shard_store.del("#{@scope}__COMMANDCNTS__{#{@name}}")
|
|
848
904
|
|
|
@@ -864,7 +920,7 @@ module OpenC3
|
|
|
864
920
|
# Delete item_map
|
|
865
921
|
item_map_key = "#{@scope}__#{@name}__item_to_packet_map"
|
|
866
922
|
db_shard_store.del(item_map_key)
|
|
867
|
-
@@item_map_cache[
|
|
923
|
+
@@item_map_cache[item_map_key] = nil
|
|
868
924
|
|
|
869
925
|
topic = { kind: 'deleted', type: 'target', name: @name }
|
|
870
926
|
# The UNKNOWN target doesn't have an associated plugin
|
|
@@ -980,6 +1036,7 @@ module OpenC3
|
|
|
980
1036
|
end
|
|
981
1037
|
CvtModel.set(json_hash, target_name: packet.target_name, packet_name: packet.packet_name, scope: @scope)
|
|
982
1038
|
end
|
|
1039
|
+
update_store_hidden_packet_names(db_shard_store, target_name, packets, type: :TLM, clear_old: clear_old)
|
|
983
1040
|
end
|
|
984
1041
|
end
|
|
985
1042
|
|
|
@@ -999,7 +1056,27 @@ module OpenC3
|
|
|
999
1056
|
raise e
|
|
1000
1057
|
end
|
|
1001
1058
|
end
|
|
1059
|
+
update_store_hidden_packet_names(db_shard_store, target_name, packets, type: :CMD, clear_old: clear_old)
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Store the names of the hidden packets so listing packet names doesn't require
|
|
1064
|
+
# parsing every packet definition. When only some packets are updated
|
|
1065
|
+
# (clear_old false) merge them into the existing list.
|
|
1066
|
+
def update_store_hidden_packet_names(db_shard_store, target_name, packets, type:, clear_old:)
|
|
1067
|
+
# e.g. DEFAULT__openc3tlm__INST__hidden
|
|
1068
|
+
key = "#{@scope}__openc3#{type.to_s.downcase}__#{target_name}__hidden"
|
|
1069
|
+
names = []
|
|
1070
|
+
unless clear_old
|
|
1071
|
+
names_json = db_shard_store.get(key)
|
|
1072
|
+
# Without an existing list hidden_packet_names builds it from all the packets
|
|
1073
|
+
return unless names_json
|
|
1074
|
+
names = JSON.parse(names_json) - packets.keys
|
|
1075
|
+
end
|
|
1076
|
+
packets.each do |packet_name, packet|
|
|
1077
|
+
names << packet_name if packet.hidden
|
|
1002
1078
|
end
|
|
1079
|
+
db_shard_store.set(key, JSON.generate(names.sort))
|
|
1003
1080
|
end
|
|
1004
1081
|
|
|
1005
1082
|
def update_store_limits_groups(system)
|
|
@@ -1027,7 +1104,7 @@ module OpenC3
|
|
|
1027
1104
|
item_map_key = "#{@scope}__#{@name}__item_to_packet_map"
|
|
1028
1105
|
item_map = self.class.build_item_to_packet_map(@name, scope: @scope)
|
|
1029
1106
|
Store.instance(db_shard: @db_shard).set(item_map_key, JSON.generate(item_map, allow_nan: true))
|
|
1030
|
-
@@item_map_cache[
|
|
1107
|
+
@@item_map_cache[item_map_key] = [Time.now, item_map]
|
|
1031
1108
|
end
|
|
1032
1109
|
|
|
1033
1110
|
def update_store(system, clear_old: true)
|
|
@@ -72,6 +72,11 @@ module OpenC3
|
|
|
72
72
|
env['PYTHONPATH'] = nil
|
|
73
73
|
end
|
|
74
74
|
env['OPENC3_MICROSERVICE_NAME'] = microservice_name
|
|
75
|
+
# Tell the microservice not to report its own cpu/memory - we do that for
|
|
76
|
+
# it in publish_process_metrics, which also covers microservices that
|
|
77
|
+
# exec() into something that never loads the OpenC3 libraries. Only claim
|
|
78
|
+
# this where we can actually deliver on it.
|
|
79
|
+
env[Metric::SUPERVISED_ENV_VAR] = '1' if ProcessStats.supported?
|
|
75
80
|
container = microservice_config["container"]
|
|
76
81
|
scope = microservice_name.split("__")[0]
|
|
77
82
|
|
|
@@ -19,6 +19,8 @@ require 'childprocess'
|
|
|
19
19
|
require 'openc3'
|
|
20
20
|
require 'fileutils'
|
|
21
21
|
require 'tempfile'
|
|
22
|
+
require 'openc3/models/metric_model'
|
|
23
|
+
require 'openc3/utilities/process_stats'
|
|
22
24
|
|
|
23
25
|
module OpenC3
|
|
24
26
|
# Class to prevent an infinitely growing log file
|
|
@@ -156,6 +158,15 @@ module OpenC3
|
|
|
156
158
|
end
|
|
157
159
|
end
|
|
158
160
|
|
|
161
|
+
# The pid of the spawned process, or nil if it isn't running. Note this is
|
|
162
|
+
# the pid of plugin_microservice.rb, which exec()s the configured cmd, so it
|
|
163
|
+
# stays the pid of whatever the microservice actually ends up running.
|
|
164
|
+
def pid
|
|
165
|
+
@process ? @process.pid : nil
|
|
166
|
+
rescue Exception
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
159
170
|
def soft_stop
|
|
160
171
|
Thread.new do
|
|
161
172
|
Logger.info("Soft shutting down process: #{cmd_line()}", scope: @scope)
|
|
@@ -255,6 +266,9 @@ module OpenC3
|
|
|
255
266
|
@new_processes = {}
|
|
256
267
|
@changed_processes = {}
|
|
257
268
|
@removed_processes = {}
|
|
269
|
+
# microservice name => ProcessStats, so cpu deltas survive across cycles
|
|
270
|
+
@process_stats = {}
|
|
271
|
+
@process_stats_db_shards = {}
|
|
258
272
|
@mutex = Mutex.new
|
|
259
273
|
@shutdown = false
|
|
260
274
|
@shutdown_complete = false
|
|
@@ -264,6 +278,60 @@ module OpenC3
|
|
|
264
278
|
raise "Implement in subclass"
|
|
265
279
|
end
|
|
266
280
|
|
|
281
|
+
# Report cpu and memory for every microservice this operator is running.
|
|
282
|
+
#
|
|
283
|
+
# Microservices that are themselves OpenC3::Microservices could report this
|
|
284
|
+
# about themselves, but ones that exec() into a Rails app, a Python script
|
|
285
|
+
# or any other binary never load the OpenC3 libraries and so can't. Doing it
|
|
286
|
+
# here covers every microservice the same way, including plugins we don't
|
|
287
|
+
# ship, without asking anything of the plugin author.
|
|
288
|
+
def publish_process_metrics
|
|
289
|
+
return unless ProcessStats.supported?
|
|
290
|
+
|
|
291
|
+
@mutex.synchronize do
|
|
292
|
+
@processes.each do |name, p|
|
|
293
|
+
break if @shutdown
|
|
294
|
+
# Not started yet - queued behind the per cycle start limit
|
|
295
|
+
next if @new_processes[name]
|
|
296
|
+
|
|
297
|
+
# Only processes this operator spawned locally have a pid we can read
|
|
298
|
+
# from /proc. The KubernetesOperator's processes are pods elsewhere in
|
|
299
|
+
# the cluster, so they don't implement pid and keep reporting their
|
|
300
|
+
# own metrics from inside the pod.
|
|
301
|
+
next unless p.respond_to?(:pid)
|
|
302
|
+
pid = p.pid
|
|
303
|
+
next unless pid
|
|
304
|
+
|
|
305
|
+
# Isolate each process so one failure doesn't starve the rest of metrics
|
|
306
|
+
begin
|
|
307
|
+
stats = @process_stats[name]
|
|
308
|
+
# A respawn gets a new pid, and the old cpu totals don't carry over
|
|
309
|
+
if stats.nil? or stats.pid != pid
|
|
310
|
+
# Look up the shard before caching stats so a failed lookup is retried next cycle
|
|
311
|
+
@process_stats_db_shards[name] = MetricModel._lookup_db_shard(name, scope: p.scope)
|
|
312
|
+
stats = ProcessStats.new(pid)
|
|
313
|
+
@process_stats[name] = stats
|
|
314
|
+
end
|
|
315
|
+
next unless stats.sample
|
|
316
|
+
|
|
317
|
+
MetricModel.set_process(
|
|
318
|
+
{ 'name' => name, 'db_shard' => @process_stats_db_shards[name], 'values' => stats.values },
|
|
319
|
+
scope: p.scope
|
|
320
|
+
)
|
|
321
|
+
rescue => e
|
|
322
|
+
Logger.error("#{self.class} failed to publish process metrics for #{name}: #{e.class} #{e.message}")
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Stop tracking microservices that have gone away. Their metrics rows
|
|
327
|
+
# expire on their own (METRIC_EXPIRE_SECONDS).
|
|
328
|
+
(@process_stats.keys - @processes.keys).each do |name|
|
|
329
|
+
@process_stats.delete(name)
|
|
330
|
+
@process_stats_db_shards.delete(name)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
267
335
|
def start_new
|
|
268
336
|
@mutex.synchronize do
|
|
269
337
|
if @new_processes.length > 0
|
|
@@ -393,6 +461,7 @@ module OpenC3
|
|
|
393
461
|
respawn_changed()
|
|
394
462
|
start_new()
|
|
395
463
|
respawn_dead()
|
|
464
|
+
publish_process_metrics()
|
|
396
465
|
rescue => e
|
|
397
466
|
Logger.error("#{self.class} cycle error, continuing: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
|
|
398
467
|
end
|
|
@@ -879,7 +879,7 @@ module OpenC3
|
|
|
879
879
|
result = nil
|
|
880
880
|
synchronize_allow_reads(true) do
|
|
881
881
|
result = read_all(value_type, buffer, false).map! do |array|
|
|
882
|
-
array << @items[array[0]].
|
|
882
|
+
array << @items[array[0]].limits_state
|
|
883
883
|
end
|
|
884
884
|
end
|
|
885
885
|
return result
|
|
@@ -996,7 +996,7 @@ module OpenC3
|
|
|
996
996
|
# This is an optimization so we don't have to iterate through all the items when
|
|
997
997
|
# checking for limits.
|
|
998
998
|
def update_limits_items_cache(item)
|
|
999
|
-
if item.
|
|
999
|
+
if item.limits_values || item.state_colors
|
|
1000
1000
|
@limits_items ||= []
|
|
1001
1001
|
@limits_items_hash ||= {}
|
|
1002
1002
|
unless @limits_items_hash[item]
|
|
@@ -1306,7 +1306,7 @@ module OpenC3
|
|
|
1306
1306
|
json_hash["#{item.name}__F"] = read_item(item, :FORMATTED, @buffer, given_raw)
|
|
1307
1307
|
end
|
|
1308
1308
|
if include_limits_states
|
|
1309
|
-
limits_state = item.
|
|
1309
|
+
limits_state = item.limits_state
|
|
1310
1310
|
end
|
|
1311
1311
|
if limits_state
|
|
1312
1312
|
json_hash["#{item.name}__L"] = limits_state
|
|
@@ -1339,6 +1339,12 @@ module OpenC3
|
|
|
1339
1339
|
return unless @buffer
|
|
1340
1340
|
return unless @obfuscated_items
|
|
1341
1341
|
|
|
1342
|
+
# Given values hold the plaintext the user sent, so drop them for obfuscated items
|
|
1343
|
+
if @given_values
|
|
1344
|
+
obfuscated_names = @obfuscated_items.map { |item| item.name.upcase }
|
|
1345
|
+
@given_values.delete_if { |key, _| obfuscated_names.include?(key.to_s.upcase) }
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1342
1348
|
@obfuscated_items.each do |item|
|
|
1343
1349
|
next if item.data_type == :DERIVED
|
|
1344
1350
|
|
|
@@ -1441,7 +1447,7 @@ module OpenC3
|
|
|
1441
1447
|
|
|
1442
1448
|
def handle_limits_states(item, value)
|
|
1443
1449
|
# Retrieve limits state for the given value
|
|
1444
|
-
limits_state = item.state_colors[value]
|
|
1450
|
+
limits_state = item.state_colors ? item.state_colors[value] : nil
|
|
1445
1451
|
|
|
1446
1452
|
if item.limits.state != limits_state # PacketItemLimits state has changed
|
|
1447
1453
|
# Save old limits state
|
|
@@ -99,7 +99,11 @@ module OpenC3
|
|
|
99
99
|
# Note: DERIVED is not a valid converted type. Also TIME is currently only a converted type
|
|
100
100
|
CONVERTED_DATA_TYPES = [:INT, :UINT, :FLOAT, :STRING, :BLOCK, :BOOL, :OBJECT, :ARRAY, :ANY, :TIME]
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
# @param descriptions [Boolean] Whether to keep packet and item descriptions.
|
|
103
|
+
# Pass false in processes that never display them (e.g. decom and
|
|
104
|
+
# interface microservices) to reduce memory on large definitions.
|
|
105
|
+
def initialize(descriptions: true)
|
|
106
|
+
@descriptions = descriptions
|
|
103
107
|
@name = nil
|
|
104
108
|
@telemetry = {}
|
|
105
109
|
@commands = {}
|
|
@@ -345,6 +349,12 @@ module OpenC3
|
|
|
345
349
|
def finish_packet
|
|
346
350
|
finish_item()
|
|
347
351
|
if @current_packet
|
|
352
|
+
unless @descriptions
|
|
353
|
+
@current_packet.description = nil
|
|
354
|
+
# Reserved items (PACKET_TIMESECONDS, etc.) get descriptions from
|
|
355
|
+
# define_reserved_items and never pass through finish_item
|
|
356
|
+
@current_packet.sorted_items.each { |item| item.description = nil }
|
|
357
|
+
end
|
|
348
358
|
@warnings += @current_packet.check_bit_offsets
|
|
349
359
|
if !@current_packet.virtual && !@current_packet.disabled && !@current_packet.catchall && @current_packet.target_name != 'UNKNOWN' && @current_packet.id_items.empty?
|
|
350
360
|
type = @current_cmd_or_tlm == COMMAND ? "Command" : "Telemetry"
|
|
@@ -926,6 +936,7 @@ module OpenC3
|
|
|
926
936
|
# Finish updating packet item
|
|
927
937
|
def finish_item
|
|
928
938
|
if @current_item
|
|
939
|
+
@current_item.description = nil unless @descriptions
|
|
929
940
|
@current_packet.set_item(@current_item)
|
|
930
941
|
if @current_cmd_or_tlm == TELEMETRY
|
|
931
942
|
target_latest_data = @latest_data[@current_packet.target_name]
|
|
@@ -85,8 +85,22 @@ module OpenC3
|
|
|
85
85
|
# The allowable state colors
|
|
86
86
|
STATE_COLORS = [:GREEN, :YELLOW, :RED]
|
|
87
87
|
|
|
88
|
+
# PacketItemLimits is created on first access since most items have no
|
|
89
|
+
# limits and large definitions have hundreds of thousands of items.
|
|
88
90
|
# @return [PacketItemLimits] All information regarding limits for this PacketItem
|
|
89
|
-
|
|
91
|
+
def limits
|
|
92
|
+
@limits ||= PacketItemLimits.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Symbol, nil] The limits state without creating PacketItemLimits
|
|
96
|
+
def limits_state
|
|
97
|
+
@limits ? @limits.state : nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Hash, nil] The limits values without creating PacketItemLimits
|
|
101
|
+
def limits_values
|
|
102
|
+
@limits ? @limits.values : nil
|
|
103
|
+
end
|
|
90
104
|
|
|
91
105
|
# @return [Boolean] Whether the parameter must be obfuscated from logs or not
|
|
92
106
|
attr_accessor :obfuscate
|
|
@@ -110,7 +124,7 @@ module OpenC3
|
|
|
110
124
|
@obfuscate = false
|
|
111
125
|
@messages_disabled = nil
|
|
112
126
|
@state_colors = nil
|
|
113
|
-
@limits =
|
|
127
|
+
@limits = nil
|
|
114
128
|
@meta = nil
|
|
115
129
|
end
|
|
116
130
|
|
|
@@ -165,7 +179,6 @@ module OpenC3
|
|
|
165
179
|
end
|
|
166
180
|
|
|
167
181
|
@states = upcase_states
|
|
168
|
-
@state_colors ||= {}
|
|
169
182
|
else
|
|
170
183
|
@states = nil
|
|
171
184
|
end
|
|
@@ -317,7 +330,7 @@ module OpenC3
|
|
|
317
330
|
item.hazardous = self.hazardous.clone if self.hazardous
|
|
318
331
|
item.messages_disabled = self.messages_disabled.clone if self.messages_disabled
|
|
319
332
|
item.state_colors = self.state_colors.clone if self.state_colors
|
|
320
|
-
item.limits =
|
|
333
|
+
item.limits = @limits.clone if @limits
|
|
321
334
|
item.meta = self.meta.clone if @meta
|
|
322
335
|
item.obfuscate = self.obfuscate.clone if @obfuscate
|
|
323
336
|
item
|
|
@@ -385,14 +398,19 @@ module OpenC3
|
|
|
385
398
|
if @states
|
|
386
399
|
@states.each do |state_name, state_value|
|
|
387
400
|
config << " STATE #{state_name.to_s.quote_if_necessary} #{state_value.to_s.quote_if_necessary}"
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
401
|
+
# Only write the attributes STATE accepts for this packet type so the config parses back
|
|
402
|
+
if cmd_or_tlm == :TELEMETRY
|
|
403
|
+
if @state_colors and @state_colors[state_name]
|
|
404
|
+
config << " #{@state_colors[state_name]}"
|
|
405
|
+
end
|
|
406
|
+
else
|
|
407
|
+
if @hazardous and @hazardous[state_name]
|
|
408
|
+
config << " HAZARDOUS"
|
|
409
|
+
config << " #{@hazardous[state_name].to_s.quote_if_necessary}" unless @hazardous[state_name].to_s.empty?
|
|
410
|
+
end
|
|
411
|
+
if @messages_disabled and @messages_disabled[state_name]
|
|
412
|
+
config << " DISABLE_MESSAGES"
|
|
413
|
+
end
|
|
396
414
|
end
|
|
397
415
|
config << "\n"
|
|
398
416
|
end
|
|
@@ -401,16 +419,16 @@ module OpenC3
|
|
|
401
419
|
config << self.read_conversion.to_config(:READ) if self.read_conversion
|
|
402
420
|
config << self.write_conversion.to_config(:WRITE) if self.write_conversion
|
|
403
421
|
|
|
404
|
-
if
|
|
405
|
-
|
|
406
|
-
config << " LIMITS #{limits_set} #{
|
|
422
|
+
if limits_values
|
|
423
|
+
@limits.values.each do |limits_set, limits_values|
|
|
424
|
+
config << " LIMITS #{limits_set} #{@limits.persistence_setting} #{@limits.enabled ? 'ENABLED' : 'DISABLED'} #{limits_values[0]} #{limits_values[1]} #{limits_values[2]} #{limits_values[3]}"
|
|
407
425
|
if limits_values[4] && limits_values[5]
|
|
408
426
|
config << " #{limits_values[4]} #{limits_values[5]}\n"
|
|
409
427
|
else
|
|
410
428
|
config << "\n"
|
|
411
429
|
end
|
|
412
430
|
end
|
|
413
|
-
config <<
|
|
431
|
+
config << @limits.response.to_config if @limits.response
|
|
414
432
|
end
|
|
415
433
|
|
|
416
434
|
if @meta
|
|
@@ -456,19 +474,19 @@ module OpenC3
|
|
|
456
474
|
config['write_conversion'] = self.write_conversion.as_json(*a) if self.write_conversion
|
|
457
475
|
|
|
458
476
|
config['limits'] ||= {}
|
|
459
|
-
if
|
|
477
|
+
if @limits && @limits.enabled
|
|
460
478
|
config['limits']['enabled'] = true
|
|
461
|
-
elsif
|
|
479
|
+
elsif limits_values || (self.state_colors && self.state_colors.length > 0)
|
|
462
480
|
# Only set to false if there are limits or state colors
|
|
463
481
|
# to avoid items without limits acting like they can be enabled
|
|
464
482
|
config['limits']['enabled'] = false
|
|
465
483
|
end
|
|
466
|
-
if
|
|
484
|
+
if limits_values
|
|
467
485
|
# Only set these if there are limits.values because persistence_setting has a default
|
|
468
486
|
# and we don't want keys on the 'limits' hash if there aren't any limits
|
|
469
|
-
config['limits']['persistence_setting'] =
|
|
470
|
-
config['limits']['response'] =
|
|
471
|
-
|
|
487
|
+
config['limits']['persistence_setting'] = @limits.persistence_setting if @limits.persistence_setting
|
|
488
|
+
config['limits']['response'] = @limits.response.to_s if @limits.response
|
|
489
|
+
@limits.values.each do |limits_set, limits_values|
|
|
472
490
|
limits = {}
|
|
473
491
|
limits['red_low'] = limits_values[0]
|
|
474
492
|
limits['yellow_low'] = limits_values[1]
|