openc3 7.4.1 → 7.5.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.
- 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 +1 -1
- 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 +6 -10
- 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 +13 -4
- data/lib/openc3/microservices/decom_microservice.rb +7 -4
- 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/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 +83 -9
- 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 +27 -14
- 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 +58 -84
- data/lib/openc3/script/extract.rb +215 -19
- data/lib/openc3/script/plugins.rb +4 -11
- 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/router_topic.rb +2 -1
- 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 +154 -2
- 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 +14 -0
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/store_autoload.rb +20 -3
- 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 +6 -6
- 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") }
|
|
@@ -751,11 +798,12 @@ module OpenC3
|
|
|
751
798
|
# Version History is an Enterprise feature. The plugin deploy runs in the
|
|
752
799
|
# cmd-tlm-api process, which (unlike script-runner-api) doesn't otherwise
|
|
753
800
|
# require the store, so lazily load it on demand. Returns false in Core
|
|
754
|
-
# builds where the Enterprise gem isn't present
|
|
801
|
+
# builds where the Enterprise gem isn't present, and when the store is
|
|
802
|
+
# disabled (OPENC3_VERSION_HISTORY_DIR unset): its commits no-op there, so
|
|
803
|
+
# versioning would drop the modified copy without preserving it.
|
|
755
804
|
def version_store_available?
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
defined?(::VersionStore) ? true : false
|
|
805
|
+
require 'openc3-enterprise/utilities/version_store' unless defined?(::VersionStore)
|
|
806
|
+
defined?(::VersionStore) && ::VersionStore.enabled? ? true : false
|
|
759
807
|
rescue LoadError
|
|
760
808
|
false
|
|
761
809
|
end
|
|
@@ -765,6 +813,8 @@ module OpenC3
|
|
|
765
813
|
# copy), commits it, removes it, then commits the incoming plugin content.
|
|
766
814
|
# Best-effort: a versioning failure must not abort the plugin upgrade.
|
|
767
815
|
def apply_upgrade_version(name, new_data, ctx)
|
|
816
|
+
# Local only targets have no modified copy in the bucket
|
|
817
|
+
return if OpenC3::LocalMode.local_only_name?(name)
|
|
768
818
|
modified_key = "#{@scope}/targets_modified/#{name}"
|
|
769
819
|
resp = @bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: modified_key)
|
|
770
820
|
return unless resp && resp.body
|
|
@@ -791,6 +841,7 @@ module OpenC3
|
|
|
791
841
|
# a modified copy exists and differs (by bytes) from the rendered plugin
|
|
792
842
|
# content. Read-only.
|
|
793
843
|
def collect_modified_diff(name, new_data, collector)
|
|
844
|
+
return if OpenC3::LocalMode.local_only_name?(name)
|
|
794
845
|
resp = @bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: "#{@scope}/targets_modified/#{name}")
|
|
795
846
|
return unless resp && resp.body
|
|
796
847
|
collector << name if resp.body.read.b != new_data.b
|
|
@@ -843,6 +894,8 @@ module OpenC3
|
|
|
843
894
|
db_shard_store = Store.instance(db_shard: @db_shard)
|
|
844
895
|
db_shard_store.del("#{@scope}__openc3tlm__#{@name}")
|
|
845
896
|
db_shard_store.del("#{@scope}__openc3cmd__#{@name}")
|
|
897
|
+
db_shard_store.del("#{@scope}__openc3tlm__#{@name}__hidden")
|
|
898
|
+
db_shard_store.del("#{@scope}__openc3cmd__#{@name}__hidden")
|
|
846
899
|
db_shard_store.del("#{@scope}__TELEMETRYCNTS__{#{@name}}")
|
|
847
900
|
db_shard_store.del("#{@scope}__COMMANDCNTS__{#{@name}}")
|
|
848
901
|
|
|
@@ -980,6 +1033,7 @@ module OpenC3
|
|
|
980
1033
|
end
|
|
981
1034
|
CvtModel.set(json_hash, target_name: packet.target_name, packet_name: packet.packet_name, scope: @scope)
|
|
982
1035
|
end
|
|
1036
|
+
update_store_hidden_packet_names(db_shard_store, target_name, packets, type: :TLM, clear_old: clear_old)
|
|
983
1037
|
end
|
|
984
1038
|
end
|
|
985
1039
|
|
|
@@ -999,7 +1053,27 @@ module OpenC3
|
|
|
999
1053
|
raise e
|
|
1000
1054
|
end
|
|
1001
1055
|
end
|
|
1056
|
+
update_store_hidden_packet_names(db_shard_store, target_name, packets, type: :CMD, clear_old: clear_old)
|
|
1057
|
+
end
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
# Store the names of the hidden packets so listing packet names doesn't require
|
|
1061
|
+
# parsing every packet definition. When only some packets are updated
|
|
1062
|
+
# (clear_old false) merge them into the existing list.
|
|
1063
|
+
def update_store_hidden_packet_names(db_shard_store, target_name, packets, type:, clear_old:)
|
|
1064
|
+
# e.g. DEFAULT__openc3tlm__INST__hidden
|
|
1065
|
+
key = "#{@scope}__openc3#{type.to_s.downcase}__#{target_name}__hidden"
|
|
1066
|
+
names = []
|
|
1067
|
+
unless clear_old
|
|
1068
|
+
names_json = db_shard_store.get(key)
|
|
1069
|
+
# Without an existing list hidden_packet_names builds it from all the packets
|
|
1070
|
+
return unless names_json
|
|
1071
|
+
names = JSON.parse(names_json) - packets.keys
|
|
1072
|
+
end
|
|
1073
|
+
packets.each do |packet_name, packet|
|
|
1074
|
+
names << packet_name if packet.hidden
|
|
1002
1075
|
end
|
|
1076
|
+
db_shard_store.set(key, JSON.generate(names.sort))
|
|
1003
1077
|
end
|
|
1004
1078
|
|
|
1005
1079
|
def update_store_limits_groups(system)
|
|
@@ -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
|
|
@@ -401,16 +414,16 @@ module OpenC3
|
|
|
401
414
|
config << self.read_conversion.to_config(:READ) if self.read_conversion
|
|
402
415
|
config << self.write_conversion.to_config(:WRITE) if self.write_conversion
|
|
403
416
|
|
|
404
|
-
if
|
|
405
|
-
|
|
406
|
-
config << " LIMITS #{limits_set} #{
|
|
417
|
+
if limits_values
|
|
418
|
+
@limits.values.each do |limits_set, limits_values|
|
|
419
|
+
config << " LIMITS #{limits_set} #{@limits.persistence_setting} #{@limits.enabled ? 'ENABLED' : 'DISABLED'} #{limits_values[0]} #{limits_values[1]} #{limits_values[2]} #{limits_values[3]}"
|
|
407
420
|
if limits_values[4] && limits_values[5]
|
|
408
421
|
config << " #{limits_values[4]} #{limits_values[5]}\n"
|
|
409
422
|
else
|
|
410
423
|
config << "\n"
|
|
411
424
|
end
|
|
412
425
|
end
|
|
413
|
-
config <<
|
|
426
|
+
config << @limits.response.to_config if @limits.response
|
|
414
427
|
end
|
|
415
428
|
|
|
416
429
|
if @meta
|
|
@@ -456,19 +469,19 @@ module OpenC3
|
|
|
456
469
|
config['write_conversion'] = self.write_conversion.as_json(*a) if self.write_conversion
|
|
457
470
|
|
|
458
471
|
config['limits'] ||= {}
|
|
459
|
-
if
|
|
472
|
+
if @limits && @limits.enabled
|
|
460
473
|
config['limits']['enabled'] = true
|
|
461
|
-
elsif
|
|
474
|
+
elsif limits_values || (self.state_colors && self.state_colors.length > 0)
|
|
462
475
|
# Only set to false if there are limits or state colors
|
|
463
476
|
# to avoid items without limits acting like they can be enabled
|
|
464
477
|
config['limits']['enabled'] = false
|
|
465
478
|
end
|
|
466
|
-
if
|
|
479
|
+
if limits_values
|
|
467
480
|
# Only set these if there are limits.values because persistence_setting has a default
|
|
468
481
|
# 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
|
-
|
|
482
|
+
config['limits']['persistence_setting'] = @limits.persistence_setting if @limits.persistence_setting
|
|
483
|
+
config['limits']['response'] = @limits.response.to_s if @limits.response
|
|
484
|
+
@limits.values.each do |limits_set, limits_values|
|
|
472
485
|
limits = {}
|
|
473
486
|
limits['red_low'] = limits_values[0]
|
|
474
487
|
limits['yellow_low'] = limits_values[1]
|
|
@@ -26,7 +26,7 @@ module OpenC3
|
|
|
26
26
|
# @param warnings [Array<String>] Array of string warnings which will be
|
|
27
27
|
# appended with any warnings found when parsing the limits
|
|
28
28
|
def self.parse(parser, packet, cmd_or_tlm, item, warnings)
|
|
29
|
-
raise parser.error("Items with LIMITS can't define STATE") if item.
|
|
29
|
+
raise parser.error("Items with LIMITS can't define STATE") if item.limits_values
|
|
30
30
|
raise parser.error("Items with UNITS can't define STATE") if item.units
|
|
31
31
|
|
|
32
32
|
parser = StateParser.new(parser)
|
|
@@ -143,7 +143,8 @@ module OpenC3
|
|
|
143
143
|
raise ArgumentError, "name must be a String but is a #{name.class}" unless String === name
|
|
144
144
|
raise ArgumentError, "name must contain at least one character" if name.empty?
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
# Dedup since many packets share the same item names
|
|
147
|
+
@name = -name.upcase
|
|
147
148
|
verify_overall() if @structure_item_constructed
|
|
148
149
|
end
|
|
149
150
|
|
|
@@ -151,7 +152,7 @@ module OpenC3
|
|
|
151
152
|
raise ArgumentError, "key must be a String but is a #{key.class}" unless String === key
|
|
152
153
|
raise ArgumentError, "key must contain at least one character" if key.empty?
|
|
153
154
|
|
|
154
|
-
@key = key
|
|
155
|
+
@key = -key
|
|
155
156
|
end
|
|
156
157
|
|
|
157
158
|
def endianness=(endianness)
|