openc3 7.2.1 → 7.4.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/Gemfile +1 -1
- data/Guardfile +0 -0
- data/LICENSE.md +0 -0
- data/README.md +0 -0
- data/Rakefile +0 -0
- data/bin/openc3cli +235 -6
- data/bin/pipinstall +5 -4
- data/bin/pipuninstall +5 -1
- data/bin/uvinstall +156 -0
- data/data/config/_array_params.yaml +0 -0
- data/data/config/_canvas_values.yaml +0 -0
- data/data/config/_graph_params.yaml +0 -0
- data/data/config/_id_items.yaml +0 -0
- data/data/config/_id_params.yaml +0 -0
- data/data/config/_interfaces.yaml +0 -0
- data/data/config/_items.yaml +0 -0
- data/data/config/_params.yaml +0 -0
- data/data/config/command.yaml +0 -0
- data/data/config/command_modifiers.yaml +2 -0
- data/data/config/command_telemetry.yaml +0 -0
- data/data/config/conversions.yaml +0 -0
- data/data/config/graph_settings.yaml +0 -0
- data/data/config/interface_modifiers.yaml +103 -1
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +9 -2
- data/data/config/param_item_modifiers.yaml +0 -0
- data/data/config/parameter_modifiers.yaml +0 -0
- data/data/config/plugins.yaml +0 -0
- data/data/config/processors.yaml +0 -0
- data/data/config/protocols.yaml +0 -0
- data/data/config/screen.yaml +2 -1
- data/data/config/settings.yaml +0 -0
- data/data/config/table_manager.yaml +0 -0
- data/data/config/table_parameter_modifiers.yaml +0 -0
- data/data/config/target.yaml +21 -0
- data/data/config/target_config.yaml +0 -0
- data/data/config/telemetry.yaml +0 -0
- data/data/config/telemetry_modifiers.yaml +2 -0
- data/data/config/tool.yaml +3 -1
- data/data/config/unknown.yaml +0 -0
- data/data/config/widgets.yaml +22 -3
- data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
- data/data/xtce_schemas/xml.xsd +116 -0
- data/ext/openc3/ext/array/array.c +0 -0
- data/ext/openc3/ext/array/extconf.rb +0 -0
- data/ext/openc3/ext/buffered_file/buffered_file.c +0 -0
- data/ext/openc3/ext/buffered_file/extconf.rb +0 -0
- data/ext/openc3/ext/burst_protocol/burst_protocol.c +6 -0
- data/ext/openc3/ext/burst_protocol/extconf.rb +0 -0
- data/ext/openc3/ext/config_parser/config_parser.c +0 -0
- data/ext/openc3/ext/config_parser/extconf.rb +0 -0
- data/ext/openc3/ext/crc/crc.c +0 -0
- data/ext/openc3/ext/crc/extconf.rb +0 -0
- data/ext/openc3/ext/openc3_io/extconf.rb +0 -0
- data/ext/openc3/ext/openc3_io/openc3_io.c +0 -0
- data/ext/openc3/ext/packet/extconf.rb +0 -0
- data/ext/openc3/ext/packet/packet.c +0 -0
- data/ext/openc3/ext/platform/extconf.rb +0 -0
- data/ext/openc3/ext/platform/platform.c +4 -1
- data/ext/openc3/ext/polynomial_conversion/extconf.rb +0 -0
- data/ext/openc3/ext/polynomial_conversion/polynomial_conversion.c +0 -0
- data/ext/openc3/ext/string/extconf.rb +0 -0
- data/ext/openc3/ext/string/string.c +0 -0
- data/ext/openc3/ext/structure/structure.c +49 -17
- data/ext/openc3/ext/tabbed_plots_config/extconf.rb +0 -0
- data/ext/openc3/ext/tabbed_plots_config/tabbed_plots_config.c +0 -0
- data/ext/openc3/ext/telemetry/extconf.rb +0 -0
- data/ext/openc3/ext/telemetry/telemetry.c +1 -1
- data/lib/cosmos.rb +0 -0
- data/lib/cosmosc2.rb +0 -0
- data/lib/openc3/accessors/accessor.rb +0 -0
- data/lib/openc3/accessors/binary_accessor.rb +63 -9
- data/lib/openc3/accessors/cbor_accessor.rb +0 -0
- data/lib/openc3/accessors/form_accessor.rb +0 -0
- data/lib/openc3/accessors/html_accessor.rb +0 -0
- data/lib/openc3/accessors/http_accessor.rb +0 -0
- data/lib/openc3/accessors/json_accessor.rb +0 -0
- data/lib/openc3/accessors/template_accessor.rb +2 -2
- data/lib/openc3/accessors/xml_accessor.rb +0 -0
- data/lib/openc3/accessors.rb +0 -0
- data/lib/openc3/api/README.md +0 -0
- data/lib/openc3/api/api.rb +0 -0
- data/lib/openc3/api/authorized_api.rb +0 -0
- data/lib/openc3/api/calendar_api.rb +0 -0
- data/lib/openc3/api/cmd_api.rb +1 -1
- data/lib/openc3/api/config_api.rb +0 -0
- data/lib/openc3/api/interface_api.rb +2 -2
- data/lib/openc3/api/limits_api.rb +27 -13
- data/lib/openc3/api/metrics_api.rb +1 -1
- data/lib/openc3/api/offline_access_api.rb +1 -1
- data/lib/openc3/api/router_api.rb +3 -3
- data/lib/openc3/api/settings_api.rb +22 -18
- data/lib/openc3/api/stash_api.rb +1 -1
- data/lib/openc3/api/target_api.rb +0 -0
- data/lib/openc3/api/tlm_api.rb +5 -4
- data/lib/openc3/bridge/bridge.rb +0 -0
- data/lib/openc3/bridge/bridge_config.rb +0 -0
- data/lib/openc3/bridge/bridge_interface_thread.rb +0 -0
- data/lib/openc3/bridge/bridge_router_thread.rb +0 -0
- data/lib/openc3/ccsds/ccsds_packet.rb +0 -0
- data/lib/openc3/ccsds/ccsds_parser.rb +0 -0
- data/lib/openc3/config/config_parser.rb +78 -17
- data/lib/openc3/config/meta_config_parser.rb +0 -0
- data/lib/openc3/conversions/bit_reverse_conversion.rb +0 -0
- data/lib/openc3/conversions/conversion.rb +0 -0
- data/lib/openc3/conversions/generic_conversion.rb +0 -0
- data/lib/openc3/conversions/ip_read_conversion.rb +0 -0
- data/lib/openc3/conversions/ip_write_conversion.rb +0 -0
- data/lib/openc3/conversions/object_read_conversion.rb +0 -0
- data/lib/openc3/conversions/object_write_conversion.rb +0 -0
- data/lib/openc3/conversions/packet_time_formatted_conversion.rb +0 -0
- data/lib/openc3/conversions/packet_time_seconds_conversion.rb +0 -0
- data/lib/openc3/conversions/polynomial_conversion.rb +0 -0
- data/lib/openc3/conversions/processor_conversion.rb +0 -0
- data/lib/openc3/conversions/received_count_conversion.rb +0 -0
- data/lib/openc3/conversions/received_time_formatted_conversion.rb +0 -0
- data/lib/openc3/conversions/received_time_seconds_conversion.rb +0 -0
- data/lib/openc3/conversions/segmented_polynomial_conversion.rb +0 -0
- data/lib/openc3/conversions/unix_time_conversion.rb +0 -0
- data/lib/openc3/conversions/unix_time_formatted_conversion.rb +0 -0
- data/lib/openc3/conversions/unix_time_seconds_conversion.rb +0 -0
- data/lib/openc3/conversions.rb +0 -0
- data/lib/openc3/core_ext/array.rb +0 -0
- data/lib/openc3/core_ext/binding.rb +0 -0
- data/lib/openc3/core_ext/class.rb +0 -0
- data/lib/openc3/core_ext/exception.rb +0 -0
- data/lib/openc3/core_ext/faraday.rb +0 -0
- data/lib/openc3/core_ext/file.rb +0 -0
- data/lib/openc3/core_ext/io.rb +0 -0
- data/lib/openc3/core_ext/kernel.rb +0 -0
- data/lib/openc3/core_ext/math.rb +0 -0
- data/lib/openc3/core_ext/matrix.rb +0 -0
- data/lib/openc3/core_ext/objectspace.rb +0 -0
- data/lib/openc3/core_ext/openc3_io.rb +0 -0
- data/lib/openc3/core_ext/range.rb +0 -0
- data/lib/openc3/core_ext/socket.rb +0 -0
- data/lib/openc3/core_ext/string.rb +0 -0
- data/lib/openc3/core_ext/stringio.rb +0 -0
- data/lib/openc3/core_ext/tempfile.rb +0 -0
- data/lib/openc3/core_ext/time.rb +0 -0
- data/lib/openc3/core_ext.rb +0 -0
- data/lib/openc3/ext/.keep +0 -0
- data/lib/openc3/interfaces/file_interface.rb +0 -0
- data/lib/openc3/interfaces/http_client_interface.rb +1 -1
- data/lib/openc3/interfaces/http_server_interface.rb +0 -0
- data/lib/openc3/interfaces/interface.rb +5 -4
- data/lib/openc3/interfaces/mqtt_interface.rb +0 -0
- data/lib/openc3/interfaces/mqtt_stream_interface.rb +2 -0
- data/lib/openc3/interfaces/protocols/burst_protocol.rb +27 -0
- data/lib/openc3/interfaces/protocols/cmd_response_protocol.rb +0 -0
- data/lib/openc3/interfaces/protocols/cobs_protocol.rb +0 -0
- data/lib/openc3/interfaces/protocols/crc_protocol.rb +0 -0
- data/lib/openc3/interfaces/protocols/fixed_protocol.rb +0 -0
- data/lib/openc3/interfaces/protocols/ignore_packet_protocol.rb +0 -0
- data/lib/openc3/interfaces/protocols/length_protocol.rb +7 -0
- data/lib/openc3/interfaces/protocols/preidentified_protocol.rb +7 -0
- data/lib/openc3/interfaces/protocols/protocol.rb +4 -4
- data/lib/openc3/interfaces/protocols/slip_protocol.rb +0 -0
- data/lib/openc3/interfaces/protocols/template_protocol.rb +2 -2
- data/lib/openc3/interfaces/protocols/terminated_protocol.rb +0 -0
- data/lib/openc3/interfaces/serial_interface.rb +2 -0
- data/lib/openc3/interfaces/simulated_target_interface.rb +0 -0
- data/lib/openc3/interfaces/stream_interface.rb +0 -0
- data/lib/openc3/interfaces/tcpip_client_interface.rb +2 -0
- data/lib/openc3/interfaces/tcpip_server_interface.rb +0 -0
- data/lib/openc3/interfaces/udp_interface.rb +16 -5
- data/lib/openc3/interfaces.rb +0 -0
- data/lib/openc3/io/buffered_file.rb +0 -0
- data/lib/openc3/io/io_multiplexer.rb +0 -0
- data/lib/openc3/io/json_api.rb +3 -2
- data/lib/openc3/io/json_api_object.rb +0 -0
- data/lib/openc3/io/json_drb.rb +44 -12
- data/lib/openc3/io/json_drb_object.rb +0 -0
- data/lib/openc3/io/json_drb_rack.rb +0 -0
- data/lib/openc3/io/json_rpc.rb +0 -2
- data/lib/openc3/io/posix_serial_driver.rb +0 -0
- data/lib/openc3/io/serial_driver.rb +0 -0
- data/lib/openc3/io/stderr.rb +0 -0
- data/lib/openc3/io/stdout.rb +0 -0
- data/lib/openc3/io/udp_sockets.rb +38 -8
- data/lib/openc3/io/win32_serial_driver.rb +1 -0
- data/lib/openc3/logs/buffered_packet_log_reader.rb +0 -0
- data/lib/openc3/logs/buffered_packet_log_writer.rb +0 -0
- data/lib/openc3/logs/log_writer.rb +43 -7
- data/lib/openc3/logs/packet_log_constants.rb +0 -0
- data/lib/openc3/logs/packet_log_reader.rb +23 -2
- data/lib/openc3/logs/packet_log_writer.rb +6 -2
- data/lib/openc3/logs/stream_log.rb +1 -1
- data/lib/openc3/logs/stream_log_pair.rb +0 -0
- data/lib/openc3/logs/text_log_writer.rb +0 -0
- data/lib/openc3/logs.rb +0 -0
- data/lib/openc3/microservices/cleanup_microservice.rb +0 -0
- data/lib/openc3/microservices/decom_common.rb +0 -0
- data/lib/openc3/microservices/decom_microservice.rb +0 -0
- data/lib/openc3/microservices/interface_decom_common.rb +2 -1
- data/lib/openc3/microservices/interface_microservice.rb +73 -26
- data/lib/openc3/microservices/log_microservice.rb +0 -0
- data/lib/openc3/microservices/microservice.rb +3 -3
- data/lib/openc3/microservices/multi_microservice.rb +1 -1
- data/lib/openc3/microservices/periodic_microservice.rb +2 -2
- data/lib/openc3/microservices/plugin_microservice.rb +0 -0
- data/lib/openc3/microservices/queue_microservice.rb +0 -0
- data/lib/openc3/microservices/router_microservice.rb +0 -0
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +0 -0
- data/lib/openc3/microservices/text_log_microservice.rb +0 -0
- data/lib/openc3/migrations/20220420190000_log_stuff.rb +0 -0
- data/lib/openc3/migrations/20221202214600_add_target_names.rb +0 -0
- data/lib/openc3/migrations/20221210174900_convert_to_multi.rb +0 -0
- data/lib/openc3/migrations/20230915000002_no_scope_log_messages.rb +0 -0
- data/lib/openc3/migrations/20231022000000_tlm_viewer_config.rb +0 -0
- data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +0 -0
- data/lib/openc3/migrations/20241208080001_no_trigger_group.rb +0 -0
- data/lib/openc3/migrations/20250108060000_news_feed.rb +0 -0
- data/lib/openc3/migrations/20250402000000_periodic_only_default.rb +0 -0
- data/lib/openc3/migrations/20260203000000_remove_store_id.rb +0 -0
- data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +0 -0
- data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +0 -0
- data/lib/openc3/migrations/20260704000000_default_bridge.rb +38 -0
- data/lib/openc3/migrations/20260731000000_system_health_json.rb +52 -0
- data/lib/openc3/models/activity_model.rb +0 -0
- data/lib/openc3/models/auth_model.rb +35 -7
- data/lib/openc3/models/bridge_model.rb +133 -0
- data/lib/openc3/models/config_keyword_model.rb +63 -0
- data/lib/openc3/models/cvt_model.rb +5 -5
- data/lib/openc3/models/db_sharded_model.rb +0 -0
- data/lib/openc3/models/environment_model.rb +0 -0
- data/lib/openc3/models/gem_model.rb +13 -10
- data/lib/openc3/models/host_interface_microservice_model.rb +114 -0
- data/lib/openc3/models/info_model.rb +0 -0
- data/lib/openc3/models/interface_model.rb +179 -20
- data/lib/openc3/models/interface_status_model.rb +0 -0
- data/lib/openc3/models/metadata_model.rb +0 -0
- data/lib/openc3/models/metric_model.rb +0 -0
- data/lib/openc3/models/microservice_model.rb +26 -18
- data/lib/openc3/models/microservice_status_model.rb +0 -0
- data/lib/openc3/models/migration_model.rb +0 -0
- data/lib/openc3/models/model.rb +0 -0
- data/lib/openc3/models/news_model.rb +0 -0
- data/lib/openc3/models/note_model.rb +0 -0
- data/lib/openc3/models/offline_access_model.rb +0 -0
- data/lib/openc3/models/ping_model.rb +0 -0
- data/lib/openc3/models/plugin_model.rb +211 -50
- data/lib/openc3/models/plugin_store_model.rb +0 -0
- data/lib/openc3/models/process_status_model.rb +0 -0
- data/lib/openc3/models/python_package_model.rb +211 -15
- data/lib/openc3/models/queue_model.rb +0 -0
- data/lib/openc3/models/reaction_model.rb +0 -0
- data/lib/openc3/models/reingest_job_model.rb +0 -0
- data/lib/openc3/models/router_model.rb +0 -0
- data/lib/openc3/models/router_status_model.rb +0 -0
- data/lib/openc3/models/scope_model.rb +85 -56
- data/lib/openc3/models/script_engine_model.rb +0 -0
- data/lib/openc3/models/script_status_model.rb +0 -0
- data/lib/openc3/models/secret_model.rb +0 -0
- data/lib/openc3/models/setting_model.rb +546 -3
- data/lib/openc3/models/sorted_model.rb +0 -0
- data/lib/openc3/models/stash_model.rb +0 -0
- data/lib/openc3/models/target_model.rb +210 -31
- data/lib/openc3/models/timeline_model.rb +0 -0
- data/lib/openc3/models/tool_config_model.rb +0 -0
- data/lib/openc3/models/tool_model.rb +34 -20
- data/lib/openc3/models/trigger_group_model.rb +0 -0
- data/lib/openc3/models/trigger_model.rb +20 -0
- data/lib/openc3/models/widget_model.rb +0 -0
- data/lib/openc3/operators/microservice_operator.rb +79 -8
- data/lib/openc3/operators/operator.rb +4 -4
- data/lib/openc3/packets/binary_accessor.rb +0 -0
- data/lib/openc3/packets/command_validator.rb +0 -0
- data/lib/openc3/packets/commands.rb +4 -4
- data/lib/openc3/packets/json_packet.rb +0 -0
- data/lib/openc3/packets/limits.rb +15 -7
- data/lib/openc3/packets/limits_response.rb +0 -0
- data/lib/openc3/packets/packet.rb +5 -1
- data/lib/openc3/packets/packet_config.rb +0 -0
- data/lib/openc3/packets/packet_item.rb +0 -0
- data/lib/openc3/packets/packet_item_limits.rb +0 -0
- data/lib/openc3/packets/parsers/format_string_parser.rb +0 -0
- data/lib/openc3/packets/parsers/limits_parser.rb +0 -0
- data/lib/openc3/packets/parsers/limits_response_parser.rb +0 -0
- data/lib/openc3/packets/parsers/packet_item_parser.rb +0 -0
- data/lib/openc3/packets/parsers/packet_parser.rb +0 -0
- data/lib/openc3/packets/parsers/processor_parser.rb +0 -0
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
- data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
- data/lib/openc3/packets/structure.rb +9 -3
- data/lib/openc3/packets/structure_item.rb +0 -0
- data/lib/openc3/packets/telemetry.rb +3 -3
- data/lib/openc3/processors/processor.rb +2 -0
- data/lib/openc3/processors/statistics_processor.rb +0 -0
- data/lib/openc3/processors/watermark_processor.rb +0 -0
- data/lib/openc3/processors.rb +0 -0
- data/lib/openc3/script/api_shared.rb +0 -0
- data/lib/openc3/script/autonomic.rb +12 -12
- data/lib/openc3/script/calendar.rb +0 -0
- data/lib/openc3/script/commands.rb +1 -1
- data/lib/openc3/script/critical_cmd.rb +0 -0
- data/lib/openc3/script/exceptions.rb +0 -0
- data/lib/openc3/script/extract.rb +20 -25
- data/lib/openc3/script/limits.rb +0 -0
- data/lib/openc3/script/metadata.rb +0 -0
- data/lib/openc3/script/packages.rb +0 -0
- data/lib/openc3/script/plugins.rb +0 -0
- data/lib/openc3/script/queue.rb +0 -0
- data/lib/openc3/script/screen.rb +0 -0
- data/lib/openc3/script/script.rb +2 -2
- data/lib/openc3/script/script_runner.rb +0 -0
- data/lib/openc3/script/storage.rb +4 -4
- data/lib/openc3/script/suite.rb +0 -0
- data/lib/openc3/script/suite_results.rb +0 -0
- data/lib/openc3/script/suite_runner.rb +26 -0
- data/lib/openc3/script/tables.rb +0 -0
- data/lib/openc3/script/telemetry.rb +9 -3
- data/lib/openc3/script/web_socket_api.rb +247 -134
- data/lib/openc3/script.rb +0 -0
- data/lib/openc3/script_engines/script_engine.rb +0 -0
- data/lib/openc3/streams/mqtt_stream.rb +0 -0
- data/lib/openc3/streams/serial_stream.rb +0 -0
- data/lib/openc3/streams/stream.rb +0 -0
- data/lib/openc3/streams/tcpip_client_stream.rb +0 -0
- data/lib/openc3/streams/tcpip_socket_stream.rb +0 -0
- data/lib/openc3/streams/web_socket_client_stream.rb +0 -0
- data/lib/openc3/subpacketizers/subpacketizer.rb +0 -0
- data/lib/openc3/system/system.rb +0 -0
- data/lib/openc3/system/target.rb +0 -0
- data/lib/openc3/system.rb +0 -0
- data/lib/openc3/tools/cmd_tlm_server/api.rb +0 -0
- data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +7 -0
- data/lib/openc3/tools/table_manager/table.rb +0 -0
- data/lib/openc3/tools/table_manager/table_config.rb +0 -0
- data/lib/openc3/tools/table_manager/table_item.rb +0 -0
- data/lib/openc3/tools/table_manager/table_item_parser.rb +0 -0
- data/lib/openc3/tools/table_manager/table_manager_core.rb +0 -0
- data/lib/openc3/tools/table_manager/table_parser.rb +0 -0
- data/lib/openc3/tools/test_runner/test.rb +0 -0
- data/lib/openc3/top_level.rb +1 -1
- data/lib/openc3/topics/autonomic_topic.rb +0 -0
- data/lib/openc3/topics/calendar_topic.rb +0 -0
- data/lib/openc3/topics/command_decom_topic.rb +16 -1
- data/lib/openc3/topics/command_topic.rb +0 -0
- data/lib/openc3/topics/config_topic.rb +0 -0
- data/lib/openc3/topics/decom_interface_topic.rb +2 -1
- data/lib/openc3/topics/interface_topic.rb +8 -2
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/topics/notebook_topic.rb +0 -0
- data/lib/openc3/topics/queue_topic.rb +0 -0
- data/lib/openc3/topics/router_topic.rb +6 -1
- data/lib/openc3/topics/system_events_topic.rb +0 -0
- data/lib/openc3/topics/telemetry_decom_topic.rb +0 -0
- data/lib/openc3/topics/telemetry_topic.rb +0 -0
- data/lib/openc3/topics/timeline_topic.rb +0 -0
- data/lib/openc3/topics/topic.rb +0 -0
- data/lib/openc3/utilities/authentication.rb +4 -4
- data/lib/openc3/utilities/authorization.rb +0 -0
- data/lib/openc3/utilities/aws_bucket.rb +12 -4
- data/lib/openc3/utilities/bucket.rb +1 -1
- data/lib/openc3/utilities/bucket_file_cache.rb +55 -22
- data/lib/openc3/utilities/bucket_require.rb +0 -0
- data/lib/openc3/utilities/bucket_utilities.rb +3 -4
- data/lib/openc3/utilities/cli_generator.rb +0 -0
- data/lib/openc3/utilities/cmd_log.rb +0 -0
- data/lib/openc3/utilities/config_overlay.rb +75 -0
- data/lib/openc3/utilities/cosmos_rails_formatter.rb +0 -0
- data/lib/openc3/utilities/crc.rb +59 -32
- data/lib/openc3/utilities/csv.rb +5 -5
- data/lib/openc3/utilities/ctrf.rb +0 -0
- data/lib/openc3/utilities/env_helper.rb +0 -0
- data/lib/openc3/utilities/local_bucket.rb +0 -0
- data/lib/openc3/utilities/local_mode.rb +36 -7
- data/lib/openc3/utilities/logger.rb +30 -8
- data/lib/openc3/utilities/message_log.rb +0 -0
- data/lib/openc3/utilities/metric.rb +0 -0
- data/lib/openc3/utilities/migration.rb +0 -0
- data/lib/openc3/utilities/open_telemetry.rb +0 -0
- data/lib/openc3/utilities/process_manager.rb +0 -0
- data/lib/openc3/utilities/pypi_url.rb +0 -0
- data/lib/openc3/utilities/python_proxy.rb +0 -0
- data/lib/openc3/utilities/python_venv.rb +135 -0
- data/lib/openc3/utilities/quaternion.rb +0 -0
- data/lib/openc3/utilities/questdb_client.rb +0 -0
- data/lib/openc3/utilities/redis_secrets.rb +0 -0
- data/lib/openc3/utilities/reingest_job.rb +0 -0
- data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
- data/lib/openc3/utilities/rubygems_url.rb +39 -0
- data/lib/openc3/utilities/running_script.rb +119 -95
- data/lib/openc3/utilities/s3_autoload.rb +5 -5
- data/lib/openc3/utilities/script.rb +52 -12
- data/lib/openc3/utilities/secrets.rb +48 -3
- data/lib/openc3/utilities/simulated_target.rb +0 -0
- data/lib/openc3/utilities/sleeper.rb +0 -0
- data/lib/openc3/utilities/store.rb +0 -0
- data/lib/openc3/utilities/store_autoload.rb +2 -2
- data/lib/openc3/utilities/store_queued.rb +0 -0
- data/lib/openc3/utilities/target_file.rb +18 -1
- data/lib/openc3/utilities/thread_manager.rb +0 -0
- data/lib/openc3/utilities/throttle.rb +0 -0
- data/lib/openc3/utilities/zip.rb +0 -0
- data/lib/openc3/utilities.rb +0 -0
- data/lib/openc3/version.rb +6 -6
- data/lib/openc3/win32/excel.rb +0 -0
- data/lib/openc3/win32/win32.rb +0 -0
- data/lib/openc3/win32/win32_main.rb +0 -0
- data/lib/openc3.rb +2 -2
- data/tasks/gemfile_stats.rake +0 -0
- data/tasks/spec.rake +0 -0
- data/templates/command_validator/command_validator.py +0 -0
- data/templates/command_validator/command_validator.rb +0 -0
- data/templates/conversion/conversion.py +0 -0
- data/templates/conversion/conversion.rb +0 -0
- data/templates/limits_response/response.py +0 -0
- data/templates/limits_response/response.rb +0 -0
- data/templates/microservice/microservices/TEMPLATE/microservice.py +0 -0
- data/templates/microservice/microservices/TEMPLATE/microservice.rb +0 -0
- data/templates/plugin/.gitignore +0 -0
- data/templates/plugin/LICENSE.md +0 -0
- data/templates/plugin/README.md +0 -0
- data/templates/plugin/Rakefile +0 -0
- data/templates/plugin/plugin.gemspec +1 -1
- data/templates/plugin/plugin.txt +0 -0
- data/templates/processor/processor.py +1 -1
- data/templates/processor/processor.rb +0 -0
- data/templates/target/targets/TARGET/cmd_tlm/cmd.txt +0 -0
- data/templates/target/targets/TARGET/cmd_tlm/tlm.txt +0 -0
- data/templates/target/targets/TARGET/lib/target.py +0 -0
- data/templates/target/targets/TARGET/lib/target.rb +0 -0
- data/templates/target/targets/TARGET/procedures/procedure.py +0 -0
- data/templates/target/targets/TARGET/procedures/procedure.rb +0 -0
- data/templates/target/targets/TARGET/public/README.txt +0 -0
- data/templates/target/targets/TARGET/screens/status.txt +0 -0
- data/templates/target/targets/TARGET/target.txt +0 -0
- data/templates/tool_angular/.editorconfig +0 -0
- data/templates/tool_angular/.gitignore +0 -0
- data/templates/tool_angular/.vscode/extensions.json +0 -0
- data/templates/tool_angular/.vscode/launch.json +0 -0
- data/templates/tool_angular/.vscode/tasks.json +0 -0
- data/templates/tool_angular/angular.json +0 -0
- data/templates/tool_angular/extra-webpack.config.js +0 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_angular/src/app/app-routing.module.ts +0 -0
- data/templates/tool_angular/src/app/app.component.html +0 -0
- data/templates/tool_angular/src/app/app.component.scss +0 -0
- data/templates/tool_angular/src/app/app.component.spec.ts +0 -0
- data/templates/tool_angular/src/app/app.component.ts +0 -0
- data/templates/tool_angular/src/app/app.module.ts +0 -0
- data/templates/tool_angular/src/app/custom-overlay-container.ts +0 -0
- data/templates/tool_angular/src/app/empty-route/empty-route.component.ts +0 -0
- data/templates/tool_angular/src/app/openc3-api.d.ts +0 -0
- data/templates/tool_angular/src/assets/.gitkeep +0 -0
- data/templates/tool_angular/src/environments/environment.prod.ts +0 -0
- data/templates/tool_angular/src/environments/environment.ts +0 -0
- data/templates/tool_angular/src/favicon.ico +0 -0
- data/templates/tool_angular/src/index.html +0 -0
- data/templates/tool_angular/src/main.single-spa.ts +0 -0
- data/templates/tool_angular/src/single-spa/asset-url.ts +0 -0
- data/templates/tool_angular/src/single-spa/single-spa-props.ts +0 -0
- data/templates/tool_angular/src/styles.scss +0 -0
- data/templates/tool_angular/tsconfig.app.json +0 -0
- data/templates/tool_angular/tsconfig.json +0 -0
- data/templates/tool_angular/tsconfig.spec.json +0 -0
- data/templates/tool_react/.eslintrc +0 -0
- data/templates/tool_react/.gitignore +0 -0
- data/templates/tool_react/.prettierignore +0 -0
- data/templates/tool_react/babel.config.json +0 -0
- data/templates/tool_react/jest.config.js +0 -0
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_react/src/openc3-tool_name.js +0 -0
- data/templates/tool_react/src/root.component.js +0 -0
- data/templates/tool_react/src/root.component.test.js +0 -0
- data/templates/tool_react/webpack.config.js +0 -0
- data/templates/tool_svelte/.gitignore +0 -0
- data/templates/tool_svelte/.prettierignore +0 -0
- data/templates/tool_svelte/.prettierrc.js +0 -0
- data/templates/tool_svelte/babel.config.js +0 -0
- data/templates/tool_svelte/build/smui.css +0 -0
- data/templates/tool_svelte/jest.config.js +0 -0
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_svelte/rollup.config.js +0 -0
- data/templates/tool_svelte/src/App.svelte +0 -0
- data/templates/tool_svelte/src/App.test.js +0 -0
- data/templates/tool_svelte/src/theme/_smui-theme.scss +0 -0
- data/templates/tool_svelte/src/tool_name.js +0 -0
- data/templates/tool_vue/.env.standalone +0 -0
- data/templates/tool_vue/.gitignore +0 -0
- data/templates/tool_vue/.prettierrc.cjs +0 -0
- data/templates/tool_vue/eslint.config.mjs +0 -0
- data/templates/tool_vue/jsconfig.json +0 -0
- data/templates/tool_vue/package.json +3 -3
- data/templates/tool_vue/src/App.vue +0 -0
- data/templates/tool_vue/src/main.js +0 -0
- data/templates/tool_vue/src/router.js +0 -0
- data/templates/tool_vue/src/tools/tool_name/tool_name.vue +0 -0
- data/templates/tool_vue/vite.config.js +1 -1
- data/templates/widget/.prettierrc.cjs +0 -0
- data/templates/widget/LICENSE.md +0 -0
- data/templates/widget/Rakefile +0 -0
- data/templates/widget/package.json +2 -2
- data/templates/widget/src/Widget.vue +0 -0
- data/templates/widget/vite.config.js +0 -0
- metadata +40 -11
- data/templates/tool_vue/.nycrc +0 -3
- data/templates/widget/.nycrc +0 -3
|
@@ -19,9 +19,11 @@ require 'rubygems'
|
|
|
19
19
|
require 'rubygems/package'
|
|
20
20
|
require 'openc3'
|
|
21
21
|
require 'openc3/utilities/bucket'
|
|
22
|
+
require 'openc3/utilities/python_venv'
|
|
22
23
|
require 'openc3/utilities/store'
|
|
23
24
|
require 'openc3/config/config_parser'
|
|
24
25
|
require 'openc3/models/model'
|
|
26
|
+
require 'openc3/models/scope_model'
|
|
25
27
|
require 'openc3/models/gem_model'
|
|
26
28
|
require 'openc3/models/target_model'
|
|
27
29
|
require 'openc3/models/interface_model'
|
|
@@ -185,11 +187,24 @@ module OpenC3
|
|
|
185
187
|
# Called by the PluginsController to create the plugin
|
|
186
188
|
# Because this uses ERB it must be run in a separate process from the API to
|
|
187
189
|
# prevent corruption and single require problems in the current process
|
|
188
|
-
|
|
190
|
+
# diff_only: dry run that renders the plugin's target files and returns the
|
|
191
|
+
# list of modified files ("TARGET/path") whose live content differs from
|
|
192
|
+
# what this plugin would deploy — used to warn before an upgrade which user
|
|
193
|
+
# modifications it would supersede. Implies validate_only (no side effects).
|
|
194
|
+
def self.install_phase2(plugin_hash, scope:, gem_file_path: nil, validate_only: false, diff_only: false)
|
|
195
|
+
# diff_only implies a dry run; derive a local flag instead of mutating
|
|
196
|
+
# the validate_only parameter.
|
|
197
|
+
dry_run = validate_only || diff_only
|
|
189
198
|
# Register plugin to aid in uninstall if install fails
|
|
190
199
|
plugin_hash.delete("existing_plugin_txt_lines")
|
|
200
|
+
# Version History upgrade hints (threaded from the admin install through
|
|
201
|
+
# update_plugin). Extracted before the splat below because PluginModel
|
|
202
|
+
# has no such attributes. version_history_files lists modified files the
|
|
203
|
+
# user chose to take from the plugin; username attributes the upgrade.
|
|
204
|
+
upgrade_username = plugin_hash.delete("username")
|
|
205
|
+
upgrade_version_files = plugin_hash.delete("version_history_files")
|
|
191
206
|
plugin_model = PluginModel.new(**(plugin_hash.transform_keys(&:to_sym)), scope: scope)
|
|
192
|
-
plugin_model.create unless
|
|
207
|
+
plugin_model.create unless dry_run
|
|
193
208
|
|
|
194
209
|
temp_dir = Dir.mktmpdir
|
|
195
210
|
begin
|
|
@@ -206,10 +221,11 @@ module OpenC3
|
|
|
206
221
|
# Attempt to remove all older versions of this same plugin before install to prevent version conflicts
|
|
207
222
|
# Especially on downgrades
|
|
208
223
|
# Leave the same version if it already exists
|
|
209
|
-
|
|
224
|
+
# Skipped for dry_run/diff_only: a dry run must not mutate gems.
|
|
225
|
+
OpenC3::GemModel.destroy_all_other_versions(File.basename(gem_file_path)) unless dry_run
|
|
210
226
|
|
|
211
227
|
# Actually install the gem now (slow)
|
|
212
|
-
OpenC3::GemModel.install(gem_file_path, scope: scope) unless
|
|
228
|
+
OpenC3::GemModel.install(gem_file_path, scope: scope) unless dry_run
|
|
213
229
|
|
|
214
230
|
# Extract gem contents
|
|
215
231
|
gem_path = File.join(temp_dir, "gem")
|
|
@@ -224,7 +240,7 @@ module OpenC3
|
|
|
224
240
|
|
|
225
241
|
plugin_model.minimum_cosmos_version = pkg.spec.metadata['openc3_cosmos_minimum_version']
|
|
226
242
|
|
|
227
|
-
# Process
|
|
243
|
+
# Process OpenC3 Store metadata
|
|
228
244
|
plugin_model.title = pkg.spec.metadata['openc3_store_title'] || pkg.spec.summary.strip
|
|
229
245
|
plugin_model.description = pkg.spec.metadata['openc3_store_description'] || pkg.spec.description.strip
|
|
230
246
|
plugin_model.licenses = pkg.spec.licenses
|
|
@@ -239,8 +255,19 @@ module OpenC3
|
|
|
239
255
|
img_path = pkg.spec.metadata['openc3_store_image'] || 'public/store_img.png'
|
|
240
256
|
img_path = nil unless File.exist?(File.join(gem_path, img_path))
|
|
241
257
|
package_name = "#{pkg.spec.name}-#{pkg.spec.version}"
|
|
258
|
+
# Build the upgrade context once the gem version is known; TargetModel
|
|
259
|
+
# deploys use it either to collect a modified-file diff (diff_only) or
|
|
260
|
+
# to version modified files taken from the plugin.
|
|
261
|
+
upgrade_context = nil
|
|
262
|
+
if diff_only
|
|
263
|
+
upgrade_context = { diff_collector: [] }
|
|
264
|
+
elsif upgrade_version_files && !upgrade_version_files.empty?
|
|
265
|
+
upgrade_context = { username: upgrade_username,
|
|
266
|
+
plugin: "#{pkg.spec.name} #{pkg.spec.version}",
|
|
267
|
+
version_files: upgrade_version_files }
|
|
268
|
+
end
|
|
242
269
|
plugin_model.img_path = File.join('gems', package_name, img_path) if img_path # convert this filesystem path to volumes mount path
|
|
243
|
-
plugin_model.update() unless
|
|
270
|
+
plugin_model.update() unless dry_run
|
|
244
271
|
|
|
245
272
|
needs_dependencies = pkg.spec.runtime_dependencies.length > 0
|
|
246
273
|
needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
|
|
@@ -250,46 +277,51 @@ module OpenC3
|
|
|
250
277
|
requirements_path = File.join(gem_path, 'requirements.txt')
|
|
251
278
|
|
|
252
279
|
if File.exist?(pyproject_path) || File.exist?(requirements_path)
|
|
253
|
-
|
|
254
|
-
pypi_url = get_setting('pypi_url', scope: scope)
|
|
255
|
-
if pypi_url
|
|
256
|
-
pypi_url += '/simple'
|
|
257
|
-
end
|
|
258
|
-
rescue => e
|
|
259
|
-
Logger.error("Failed to retrieve pypi_url: #{e.formatted}")
|
|
260
|
-
ensure
|
|
261
|
-
if pypi_url.nil?
|
|
262
|
-
# If Redis isn't running try the ENV, then simply pypi.org/simple
|
|
263
|
-
pypi_url = ENV['PYPI_URL']
|
|
264
|
-
if pypi_url
|
|
265
|
-
pypi_url += '/simple'
|
|
266
|
-
end
|
|
267
|
-
pypi_url ||= PypiUrl::DEFAULT
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
pypi_url = PypiUrl.validate(pypi_url)
|
|
280
|
+
pypi_url = resolve_pypi_url(scope: scope)
|
|
271
281
|
unless validate_only
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
Logger.info "Installing python packages
|
|
283
|
-
|
|
282
|
+
pypi_args = build_pypi_args(pypi_url)
|
|
283
|
+
|
|
284
|
+
# Install Python dependencies into an isolated per-plugin venv when UV
|
|
285
|
+
# is available. Each plugin gets its own venv at /gems/plugin_venvs/<name>/.venv
|
|
286
|
+
# so that plugins with conflicting Python dependency versions don't interfere.
|
|
287
|
+
# If UV is unavailable or the install fails, fall back to the shared pipinstall
|
|
288
|
+
# which installs into PYTHONUSERBASE (the legacy shared environment).
|
|
289
|
+
uv_installed = ENV['OPENC3_USE_UV'] != 'false' && system('which uv > /dev/null 2>&1')
|
|
290
|
+
if uv_installed
|
|
291
|
+
plugin_venv_name = plugin_venv_name(scope: scope, plugin_name: plugin_model.name)
|
|
292
|
+
Logger.info "Installing python packages into per-plugin venv '#{plugin_venv_name}' with pypi_url=#{pypi_url}"
|
|
293
|
+
uv_args = [plugin_venv_name, gem_path] + pypi_args
|
|
294
|
+
output, status = Open3.capture2e("/openc3/bin/uvinstall", *uv_args)
|
|
295
|
+
puts output
|
|
296
|
+
if status.success?
|
|
297
|
+
# A plugin that declares openc3 itself would shadow the system
|
|
298
|
+
# library once its site-packages goes on PYTHONPATH, so drop it
|
|
299
|
+
# here rather than let the wrong client reach a running script.
|
|
300
|
+
PythonVenv.purge_reserved_packages(File.join('/gems', 'plugin_venvs', plugin_venv_name, '.venv'))
|
|
301
|
+
else
|
|
302
|
+
Logger.warn "UV per-plugin install failed, falling back to shared pipinstall"
|
|
303
|
+
uv_installed = false
|
|
304
|
+
end
|
|
284
305
|
end
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
306
|
+
|
|
307
|
+
unless uv_installed
|
|
308
|
+
pip_args = pypi_args.dup
|
|
309
|
+
if File.exist?(pyproject_path)
|
|
310
|
+
Logger.info "Installing python packages from pyproject.toml with pypi_url=#{pypi_url}"
|
|
311
|
+
pip_args << gem_path
|
|
312
|
+
else
|
|
313
|
+
Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
|
|
314
|
+
pip_args += ["-r", requirements_path]
|
|
315
|
+
end
|
|
316
|
+
# Capture output and check exit code so failures surface as a warning
|
|
317
|
+
# rather than silently succeeding. pipinstall is non-fatal: the plugin
|
|
318
|
+
# continues to install even if Python packages fail so that non-Python
|
|
319
|
+
# functionality still works.
|
|
320
|
+
output, status = Open3.capture2e("/openc3/bin/pipinstall", *pip_args)
|
|
321
|
+
puts output
|
|
322
|
+
unless status.success?
|
|
323
|
+
Logger.warn "Python package installation failed. Plugin Python microservices may not function correctly."
|
|
324
|
+
end
|
|
293
325
|
end
|
|
294
326
|
end
|
|
295
327
|
needs_dependencies = true
|
|
@@ -304,7 +336,7 @@ module OpenC3
|
|
|
304
336
|
end
|
|
305
337
|
if needs_dependencies
|
|
306
338
|
plugin_model.needs_dependencies = true
|
|
307
|
-
plugin_model.update unless
|
|
339
|
+
plugin_model.update unless dry_run
|
|
308
340
|
end
|
|
309
341
|
|
|
310
342
|
# Temporarily add all lib folders from the gem to the end of the load path
|
|
@@ -343,8 +375,12 @@ module OpenC3
|
|
|
343
375
|
when 'TARGET', 'INTERFACE', 'ROUTER', 'MICROSERVICE', 'TOOL', 'WIDGET', 'SCRIPT_ENGINE'
|
|
344
376
|
begin
|
|
345
377
|
if current_model
|
|
346
|
-
current_model.create unless
|
|
347
|
-
current_model.
|
|
378
|
+
current_model.create unless dry_run
|
|
379
|
+
if current_model.is_a?(OpenC3::TargetModel)
|
|
380
|
+
current_model.deploy(gem_path, erb_variables, validate_only: dry_run, upgrade_context: upgrade_context)
|
|
381
|
+
else
|
|
382
|
+
current_model.deploy(gem_path, erb_variables, validate_only: dry_run)
|
|
383
|
+
end
|
|
348
384
|
end
|
|
349
385
|
# If something goes wrong in create, or more likely in deploy,
|
|
350
386
|
# we want to clear the current_model and try to instantiate the next
|
|
@@ -363,8 +399,12 @@ module OpenC3
|
|
|
363
399
|
end
|
|
364
400
|
end
|
|
365
401
|
if current_model
|
|
366
|
-
current_model.create unless
|
|
367
|
-
current_model.
|
|
402
|
+
current_model.create unless dry_run
|
|
403
|
+
if current_model.is_a?(OpenC3::TargetModel)
|
|
404
|
+
current_model.deploy(gem_path, erb_variables, validate_only: dry_run, upgrade_context: upgrade_context)
|
|
405
|
+
else
|
|
406
|
+
current_model.deploy(gem_path, erb_variables, validate_only: dry_run)
|
|
407
|
+
end
|
|
368
408
|
current_model = nil
|
|
369
409
|
end
|
|
370
410
|
end
|
|
@@ -375,15 +415,26 @@ module OpenC3
|
|
|
375
415
|
end
|
|
376
416
|
rescue => e
|
|
377
417
|
# Install failed - need to cleanup
|
|
378
|
-
plugin_model.destroy unless
|
|
418
|
+
plugin_model.destroy unless dry_run
|
|
379
419
|
raise e
|
|
380
420
|
ensure
|
|
381
421
|
FileUtils.remove_entry_secure(temp_dir, true)
|
|
382
422
|
tf.unlink if tf
|
|
383
423
|
end
|
|
424
|
+
return upgrade_context[:diff_collector].uniq if diff_only
|
|
384
425
|
return plugin_model.as_json()
|
|
385
426
|
end
|
|
386
427
|
|
|
428
|
+
# Dry run: which modified files would this plugin's install supersede?
|
|
429
|
+
# Returns a list of "TARGET/path" names whose live (modified) content
|
|
430
|
+
# differs from the rendered plugin content. Read-only; no side effects.
|
|
431
|
+
def self.modified_diff(plugin_hash, scope:)
|
|
432
|
+
install_phase2(plugin_hash, scope: scope, diff_only: true)
|
|
433
|
+
rescue => e
|
|
434
|
+
Logger.warn("PluginModel.modified_diff failed: #{e.message}")
|
|
435
|
+
[]
|
|
436
|
+
end
|
|
437
|
+
|
|
387
438
|
def initialize(
|
|
388
439
|
name:,
|
|
389
440
|
variables: {},
|
|
@@ -482,6 +533,19 @@ module OpenC3
|
|
|
482
533
|
errors << e
|
|
483
534
|
end
|
|
484
535
|
end
|
|
536
|
+
# Remove the per-plugin UV virtual environment directory that was created
|
|
537
|
+
# during install_phase2. This cleans up the .venv, pyproject.toml, uv.lock,
|
|
538
|
+
# and .uv_managed marker so disk space is reclaimed on plugin uninstall.
|
|
539
|
+
begin
|
|
540
|
+
plugin_venv_name = self.class.plugin_venv_name(scope: @scope, plugin_name: @name)
|
|
541
|
+
plugin_venv_path = File.join('/gems', 'plugin_venvs', plugin_venv_name)
|
|
542
|
+
if File.directory?(plugin_venv_path)
|
|
543
|
+
Logger.info("Removing per-plugin Python venv: #{plugin_venv_path}")
|
|
544
|
+
FileUtils.rm_rf(plugin_venv_path)
|
|
545
|
+
end
|
|
546
|
+
rescue Exception => e
|
|
547
|
+
errors << e
|
|
548
|
+
end
|
|
485
549
|
# Raise all the errors at once
|
|
486
550
|
if errors.length > 0
|
|
487
551
|
message = ''
|
|
@@ -528,6 +592,103 @@ module OpenC3
|
|
|
528
592
|
return result.sort
|
|
529
593
|
end
|
|
530
594
|
|
|
595
|
+
# Resolve the PyPI URL from settings, environment, or default.
|
|
596
|
+
# Used by both install_phase2 and migrate_to_uv! to avoid duplication.
|
|
597
|
+
def self.resolve_pypi_url(scope:)
|
|
598
|
+
pypi_url = nil
|
|
599
|
+
begin
|
|
600
|
+
pypi_url = get_setting('pypi_url', scope: scope)
|
|
601
|
+
pypi_url += '/simple' if pypi_url
|
|
602
|
+
rescue => e
|
|
603
|
+
Logger.error("Failed to retrieve pypi_url: #{e.formatted}")
|
|
604
|
+
ensure
|
|
605
|
+
if pypi_url.nil?
|
|
606
|
+
pypi_url = ENV.fetch('PYPI_URL', nil)
|
|
607
|
+
pypi_url += '/simple' if pypi_url
|
|
608
|
+
pypi_url ||= PypiUrl::DEFAULT
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
PypiUrl.validate(pypi_url)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Build the argv array for pypi index and trusted-host arguments.
|
|
615
|
+
def self.build_pypi_args(pypi_url)
|
|
616
|
+
args = ["-i", pypi_url]
|
|
617
|
+
args += ["--trusted-host", URI.parse(pypi_url).host] unless ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
618
|
+
args
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# Build a sanitized venv directory name from scope and plugin name.
|
|
622
|
+
# Replaces characters that are not alphanumeric, underscore, or hyphen with underscores.
|
|
623
|
+
def self.plugin_venv_name(scope:, plugin_name:)
|
|
624
|
+
"#{scope}__#{plugin_name}".tr('^a-zA-Z0-9_-', '_')
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Check if this plugin needs migration to a per-plugin UV virtual environment.
|
|
628
|
+
# Returns true if the plugin has Python dependencies but no .uv_managed marker exists.
|
|
629
|
+
def needs_uv_migration?
|
|
630
|
+
return false unless @needs_dependencies
|
|
631
|
+
|
|
632
|
+
plugin_venv_name = self.class.plugin_venv_name(scope: @scope, plugin_name: @name)
|
|
633
|
+
marker_path = File.join('/gems', 'plugin_venvs', plugin_venv_name, '.uv_managed')
|
|
634
|
+
!File.exist?(marker_path)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Migrate this plugin from the shared Python venv to a per-plugin UV virtual environment.
|
|
638
|
+
# Non-fatal: logs a warning on failure, plugin continues using the shared venv.
|
|
639
|
+
# Returns true on success, false on failure.
|
|
640
|
+
def migrate_to_uv!(scope:)
|
|
641
|
+
plugin_venv_name = self.class.plugin_venv_name(scope: scope, plugin_name: @name)
|
|
642
|
+
marker_path = File.join('/gems', 'plugin_venvs', plugin_venv_name, '.uv_managed')
|
|
643
|
+
if File.exist?(marker_path)
|
|
644
|
+
Logger.info("Plugin '#{@name}' is already migrated to a per-plugin UV venv")
|
|
645
|
+
return true
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
gem_name = @name.split("__")[0]
|
|
649
|
+
gem_file_path = OpenC3::GemModel.get(gem_name)
|
|
650
|
+
|
|
651
|
+
temp_dir = Dir.mktmpdir
|
|
652
|
+
begin
|
|
653
|
+
# Extract gem contents (same pattern as install_phase2)
|
|
654
|
+
gem_path = File.join(temp_dir, "gem")
|
|
655
|
+
FileUtils.mkdir_p(gem_path)
|
|
656
|
+
pkg = Gem::Package.new(gem_file_path)
|
|
657
|
+
pkg.extract_files(gem_path)
|
|
658
|
+
|
|
659
|
+
# Check for Python dependency files
|
|
660
|
+
pyproject_path = File.join(gem_path, 'pyproject.toml')
|
|
661
|
+
requirements_path = File.join(gem_path, 'requirements.txt')
|
|
662
|
+
|
|
663
|
+
unless File.exist?(pyproject_path) || File.exist?(requirements_path)
|
|
664
|
+
Logger.info("Plugin #{@name} has no Python dependencies to migrate")
|
|
665
|
+
return true
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
pypi_url = self.class.resolve_pypi_url(scope: scope)
|
|
669
|
+
pypi_args = self.class.build_pypi_args(pypi_url)
|
|
670
|
+
|
|
671
|
+
# Run uvinstall for this plugin
|
|
672
|
+
plugin_venv_name = self.class.plugin_venv_name(scope: @scope, plugin_name: @name)
|
|
673
|
+
Logger.info("Migrating plugin '#{@name}' to per-plugin UV venv '#{plugin_venv_name}'")
|
|
674
|
+
uv_args = [plugin_venv_name, gem_path] + pypi_args
|
|
675
|
+
output, status = Open3.capture2e("/openc3/bin/uvinstall", *uv_args)
|
|
676
|
+
puts output
|
|
677
|
+
if status.success?
|
|
678
|
+
Logger.info("Successfully migrated plugin '#{@name}' to per-plugin UV venv")
|
|
679
|
+
return true
|
|
680
|
+
else
|
|
681
|
+
Logger.warn("UV migration failed for plugin '#{@name}'. Plugin will continue using shared venv.")
|
|
682
|
+
return false
|
|
683
|
+
end
|
|
684
|
+
rescue => e
|
|
685
|
+
Logger.warn("UV migration failed for plugin '#{@name}': #{e.message}. Plugin will continue using shared venv.")
|
|
686
|
+
return false
|
|
687
|
+
ensure
|
|
688
|
+
FileUtils.remove_entry_secure(temp_dir, true)
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
531
692
|
# Remove the backing gem for an unloaded plugin so it disappears from
|
|
532
693
|
# GemModel.names (and the admin Packages tab). Skips the removal if any
|
|
533
694
|
# other PluginModel (any scope, any counter) still references the gem,
|
|
File without changes
|
|
File without changes
|
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
# if purchased from OpenC3, Inc.
|
|
13
13
|
|
|
14
14
|
require 'fileutils'
|
|
15
|
+
require 'open3'
|
|
16
|
+
require 'set'
|
|
15
17
|
require 'openc3/utilities/process_manager'
|
|
16
18
|
require 'openc3/utilities/pypi_url'
|
|
17
19
|
require 'openc3/api/api'
|
|
18
20
|
require 'pathname'
|
|
21
|
+
require 'json'
|
|
19
22
|
|
|
20
23
|
module OpenC3
|
|
21
24
|
# This class acts like a Model but doesn't inherit from Model because it doesn't
|
|
@@ -26,34 +29,191 @@ module OpenC3
|
|
|
26
29
|
extend Api
|
|
27
30
|
|
|
28
31
|
DIST_INFO = '.dist-info'
|
|
32
|
+
PLUGIN_VENVS_DIR = '/gems/plugin_venvs' # Per-plugin isolated venvs created by uvinstall
|
|
33
|
+
SYSTEM_VENV_DIR = '/openc3/python/.venv' # Core openc3 Python library venv (read-only)
|
|
34
|
+
DEFAULT_UV_CACHE_DIR = '/gems/uv' # UV wheel cache, seeded from the Docker image at init
|
|
35
|
+
UPLOADS_DIR_NAME = 'uploads' # Subdirectory under UV cache for uploaded .whl files
|
|
36
|
+
|
|
37
|
+
# Normalize a package name to lowercase with hyphens (PEP 503 canonical form)
|
|
38
|
+
def self.normalize_pkg_name(name)
|
|
39
|
+
name.tr('_', '-').downcase
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Parse a PEP 427 wheel filename into [normalized_name, version].
|
|
43
|
+
# Handles browser-appended duplicate suffixes like "(1)" on the stem.
|
|
44
|
+
# Returns nil for non-wheel files or malformed names.
|
|
45
|
+
# Example: "numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl" => ["numpy", "2.4.6"]
|
|
46
|
+
def self.parse_wheel_filename(filename)
|
|
47
|
+
# Strip browser duplicate suffixes like " (1)" before the .whl extension
|
|
48
|
+
# Two steps to avoid any space quantifier in the regex:
|
|
49
|
+
# 1. Remove "(N).whl" at end, replacing with just ".whl"
|
|
50
|
+
# 2. Clean up leftover spaces before .whl with rstrip
|
|
51
|
+
clean = filename.sub(/\(\d+\)\.whl\z/i, '.whl')
|
|
52
|
+
clean = "#{clean.chomp('.whl').rstrip}.whl" if clean.end_with?('.whl')
|
|
53
|
+
return nil unless clean.end_with?('.whl')
|
|
54
|
+
|
|
55
|
+
# PEP 427: {name}-{version}(-{build})?-{python}-{abi}-{platform}.whl
|
|
56
|
+
# We need at least name-version-python-abi-platform (5 segments)
|
|
57
|
+
stem = clean.chomp('.whl')
|
|
58
|
+
parts = stem.split('-')
|
|
59
|
+
return nil if parts.length < 5
|
|
60
|
+
|
|
61
|
+
# The version is always the second segment; everything before it is the name
|
|
62
|
+
# (some packages have hyphens in their name that become underscores in the wheel)
|
|
63
|
+
# PEP 427 guarantees: last 3 segments are python-abi-platform,
|
|
64
|
+
# optional build tag before those, then version, then name (may have multiple segments)
|
|
65
|
+
# Simplest reliable approach: version is always at index 1
|
|
66
|
+
name = normalize_pkg_name(parts[0])
|
|
67
|
+
version = parts[1]
|
|
68
|
+
|
|
69
|
+
# Sanity check: version should start with a digit
|
|
70
|
+
return nil unless version.match?(/\A\d/)
|
|
71
|
+
|
|
72
|
+
[name, version]
|
|
73
|
+
end
|
|
29
74
|
|
|
30
75
|
def self.names
|
|
31
|
-
|
|
32
|
-
|
|
76
|
+
result = {}
|
|
77
|
+
|
|
78
|
+
# Collect all packages available in the UV download cache
|
|
79
|
+
# This includes system packages (seeded from the Docker image) plus
|
|
80
|
+
# any additional packages downloaded during plugin installs
|
|
81
|
+
cached = cached_packages
|
|
82
|
+
result['cached'] = cached.sort unless cached.empty?
|
|
83
|
+
|
|
84
|
+
# Collect packages from per-plugin venvs
|
|
85
|
+
if File.directory?(PLUGIN_VENVS_DIR)
|
|
86
|
+
Dir.glob("#{PLUGIN_VENVS_DIR}/*/").each do |plugin_dir|
|
|
87
|
+
plugin_name = File.basename(plugin_dir)
|
|
88
|
+
venv_dir = File.join(plugin_dir, '.venv')
|
|
89
|
+
next unless File.directory?(venv_dir)
|
|
90
|
+
|
|
91
|
+
# Always include plugin venvs even if empty so they remain visible
|
|
92
|
+
# in the Admin UI and selectable as install targets
|
|
93
|
+
packages = packages_in_venv(venv_dir)
|
|
94
|
+
result[plugin_name] = packages.sort
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Also collect packages from the shared venv for backwards compatibility
|
|
99
|
+
shared_packages = shared_venv_packages
|
|
100
|
+
result['shared'] = shared_packages.sort unless shared_packages.empty?
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# List packages in a specific venv by scanning dist-info directories.
|
|
106
|
+
# Returns normalized names (lowercase, hyphens) for consistent display.
|
|
107
|
+
def self.packages_in_venv(venv_dir)
|
|
108
|
+
packages = []
|
|
109
|
+
# Look for site-packages in the venv's lib directory
|
|
110
|
+
Dir.glob("#{venv_dir}/lib/*/site-packages").each do |site_packages|
|
|
111
|
+
next unless File.directory?(site_packages)
|
|
112
|
+
Pathname.new(site_packages).children.each do |child|
|
|
113
|
+
if child.directory? && File.extname(child) == DIST_INFO
|
|
114
|
+
raw_name = File.basename(child, DIST_INFO)
|
|
115
|
+
# Normalize: split name from version, normalize name, rejoin
|
|
116
|
+
match = raw_name.match(/\A(.+?)-(\d.*)/)
|
|
117
|
+
if match
|
|
118
|
+
packages << "#{normalize_pkg_name(match[1])}-#{match[2]}"
|
|
119
|
+
else
|
|
120
|
+
packages << normalize_pkg_name(raw_name)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
packages
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# List packages in the system venv (/openc3/python/.venv)
|
|
129
|
+
def self.system_venv_packages
|
|
130
|
+
packages_in_venv(SYSTEM_VENV_DIR)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# List unique packages in the UV download cache.
|
|
134
|
+
# UV cache structure: wheels-v<N>/<registry>/<package-name>/<version-pytag-abitag-platform>
|
|
135
|
+
# e.g. wheels-v6/pypi/numpy/2.4.6-cp312-cp312-musllinux_1_2_aarch64
|
|
136
|
+
# Also scans the uploads/ subdirectory for user-uploaded .whl files.
|
|
137
|
+
def self.cached_packages
|
|
138
|
+
cache_dir = ENV.fetch('UV_CACHE_DIR', DEFAULT_UV_CACHE_DIR)
|
|
139
|
+
return [] unless File.directory?(cache_dir)
|
|
140
|
+
|
|
141
|
+
packages = Set.new
|
|
142
|
+
# Glob 4 levels deep: wheels-v<N>/<registry>/<package-name>/<version-entry>
|
|
143
|
+
Dir.glob("#{cache_dir}/wheels-v*/*/*/*").each do |entry|
|
|
144
|
+
basename = File.basename(entry)
|
|
145
|
+
# Skip UV metadata sidecar files (.http, .msgpack)
|
|
146
|
+
next if basename.end_with?('.http', '.msgpack')
|
|
147
|
+
|
|
148
|
+
# Version entries start with a digit
|
|
149
|
+
match = basename.match(/\A(\d[^-]*)/)
|
|
150
|
+
next unless match
|
|
151
|
+
|
|
152
|
+
# Parent directory name is the package name
|
|
153
|
+
pkg_name = File.basename(File.dirname(entry)).tr('_', '-').downcase
|
|
154
|
+
packages.add("#{pkg_name}-#{match[1]}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Scan uploaded wheels stored at <cache_dir>/uploads/*.whl
|
|
158
|
+
uploads_dir = File.join(cache_dir, UPLOADS_DIR_NAME)
|
|
159
|
+
if File.directory?(uploads_dir)
|
|
160
|
+
Dir.glob("#{uploads_dir}/*.whl").each do |whl_path|
|
|
161
|
+
parsed = parse_wheel_filename(File.basename(whl_path))
|
|
162
|
+
next unless parsed
|
|
163
|
+
|
|
164
|
+
packages.add("#{parsed[0]}-#{parsed[1]}")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
packages.to_a
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# List packages in the shared venv (backwards compatibility)
|
|
172
|
+
def self.shared_venv_packages
|
|
173
|
+
packages = []
|
|
174
|
+
pythonuserbase = ENV.fetch('PYTHONUSERBASE', nil)
|
|
175
|
+
return packages unless pythonuserbase
|
|
176
|
+
|
|
177
|
+
paths = Dir.glob("#{pythonuserbase}/lib/*")
|
|
33
178
|
paths.each do |path|
|
|
34
|
-
|
|
179
|
+
site_packages = File.join(path, 'site-packages')
|
|
180
|
+
next unless File.directory?(site_packages)
|
|
181
|
+
Pathname.new(site_packages).children.each do |child|
|
|
182
|
+
if child.directory? && File.extname(child) == DIST_INFO
|
|
183
|
+
packages << File.basename(child, DIST_INFO)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
35
186
|
end
|
|
36
|
-
|
|
187
|
+
packages
|
|
37
188
|
end
|
|
38
189
|
|
|
39
190
|
def self.get(name)
|
|
40
|
-
path = "#{ENV
|
|
191
|
+
path = "#{ENV.fetch('PYTHONUSERBASE', nil)}/cache"
|
|
41
192
|
FileUtils.mkdir_p(path) unless Dir.exist?(path)
|
|
42
193
|
result = Pathname.new(path).children.select { |c| c.file? and File.basename(c, File.extname(c)) == name }
|
|
43
194
|
if result.length > 0
|
|
44
195
|
return result[0] if File.exist?(result[0])
|
|
45
196
|
end
|
|
46
|
-
raise "Package #{name} not found"
|
|
197
|
+
raise "Package '#{name}' not found"
|
|
47
198
|
end
|
|
48
199
|
|
|
49
|
-
def self.put(package_file_path, package_install: true, scope:)
|
|
200
|
+
def self.put(package_file_path, package_install: true, scope:, plugin: nil)
|
|
50
201
|
if File.file?(package_file_path)
|
|
51
202
|
package_filename = File.basename(package_file_path)
|
|
52
|
-
FileUtils.mkdir_p("#{ENV
|
|
53
|
-
cache_path = "#{ENV
|
|
203
|
+
FileUtils.mkdir_p("#{ENV.fetch('PYTHONUSERBASE')}/cache") unless Dir.exist?("#{ENV.fetch('PYTHONUSERBASE')}/cache")
|
|
204
|
+
cache_path = "#{ENV.fetch('PYTHONUSERBASE')}/cache/#{File.basename(package_file_path)}"
|
|
54
205
|
FileUtils.cp(package_file_path, cache_path)
|
|
206
|
+
|
|
207
|
+
# Copy uploaded .whl files to the UV uploads directory so they appear
|
|
208
|
+
# in the "Cached" section of the Admin Packages UI
|
|
209
|
+
if package_filename.end_with?('.whl')
|
|
210
|
+
uploads_dir = File.join(ENV.fetch('UV_CACHE_DIR', DEFAULT_UV_CACHE_DIR), UPLOADS_DIR_NAME)
|
|
211
|
+
FileUtils.mkdir_p(uploads_dir)
|
|
212
|
+
FileUtils.cp(package_file_path, File.join(uploads_dir, package_filename))
|
|
213
|
+
end
|
|
214
|
+
|
|
55
215
|
if package_install
|
|
56
|
-
return self.install(cache_path, scope: scope)
|
|
216
|
+
return self.install(cache_path, scope: scope, plugin: plugin)
|
|
57
217
|
end
|
|
58
218
|
else
|
|
59
219
|
message = "Package file #{package_file_path} does not exist!"
|
|
@@ -63,7 +223,11 @@ module OpenC3
|
|
|
63
223
|
return nil
|
|
64
224
|
end
|
|
65
225
|
|
|
66
|
-
|
|
226
|
+
# Install a Python package via pipinstall. When +plugin+ is provided, the
|
|
227
|
+
# package is installed into that plugin's per-plugin venv instead of the
|
|
228
|
+
# shared PYTHONUSERBASE. This is used by the Admin Packages tab when a user
|
|
229
|
+
# selects a specific plugin venv as the install target.
|
|
230
|
+
def self.install(name_or_path, scope:, plugin: nil)
|
|
67
231
|
if File.exist?(name_or_path)
|
|
68
232
|
package_file_path = name_or_path
|
|
69
233
|
else
|
|
@@ -80,7 +244,7 @@ module OpenC3
|
|
|
80
244
|
ensure
|
|
81
245
|
if pypi_url.nil?
|
|
82
246
|
# If Redis isn't running try the ENV, then simply pypi.org/simple
|
|
83
|
-
pypi_url = ENV
|
|
247
|
+
pypi_url = ENV.fetch('PYPI_URL', nil)
|
|
84
248
|
if pypi_url
|
|
85
249
|
pypi_url += '/simple'
|
|
86
250
|
end
|
|
@@ -94,18 +258,50 @@ module OpenC3
|
|
|
94
258
|
else
|
|
95
259
|
pip_args = ["-i", pypi_url, "--trusted-host", URI.parse(pypi_url).host, package_file_path]
|
|
96
260
|
end
|
|
97
|
-
|
|
261
|
+
spawn_env = {}
|
|
262
|
+
if plugin
|
|
263
|
+
venv_path = "#{PLUGIN_VENVS_DIR}/#{plugin}/.venv"
|
|
264
|
+
spawn_env['PIPINSTALL_VENV'] = venv_path
|
|
265
|
+
end
|
|
266
|
+
result = OpenC3::ProcessManager.instance.spawn(["/openc3/bin/pipinstall"] + pip_args, "package_install", package_filename, Time.now + 3600.0, scope: scope, env: spawn_env)
|
|
98
267
|
return result.name
|
|
99
268
|
end
|
|
100
269
|
|
|
101
|
-
|
|
270
|
+
# Uninstall a Python package. When +plugin+ is provided, the package is
|
|
271
|
+
# removed from that plugin's per-plugin venv; otherwise from the shared venv.
|
|
272
|
+
def self.destroy(name, scope:, plugin: nil)
|
|
102
273
|
package_name, version = self.extract_name_and_version(name)
|
|
103
274
|
Logger.info "Uninstalling package: #{name}"
|
|
104
275
|
pip_args = [package_name]
|
|
105
|
-
|
|
276
|
+
spawn_env = {}
|
|
277
|
+
if plugin
|
|
278
|
+
venv_path = "#{PLUGIN_VENVS_DIR}/#{plugin}/.venv"
|
|
279
|
+
spawn_env['PIPINSTALL_VENV'] = venv_path
|
|
280
|
+
end
|
|
281
|
+
result = OpenC3::ProcessManager.instance.spawn(["/openc3/bin/pipuninstall"] + pip_args, "package_uninstall", name, Time.now + 3600.0, scope: scope, env: spawn_env)
|
|
106
282
|
return result.name
|
|
107
283
|
end
|
|
108
284
|
|
|
285
|
+
# Returns a hash of plugin_name => "uv pip list" text output for each plugin venv.
|
|
286
|
+
def self.trees
|
|
287
|
+
result = {}
|
|
288
|
+
|
|
289
|
+
if File.directory?(PLUGIN_VENVS_DIR)
|
|
290
|
+
Dir.glob("#{PLUGIN_VENVS_DIR}/*/").each do |plugin_dir|
|
|
291
|
+
plugin_name = File.basename(plugin_dir)
|
|
292
|
+
venv_dir = File.join(plugin_dir, '.venv')
|
|
293
|
+
next unless File.directory?(venv_dir)
|
|
294
|
+
|
|
295
|
+
stdout, status = Open3.capture2('uv', 'pip', 'list', '--python', venv_dir)
|
|
296
|
+
if status.success? && stdout.lines.length > 2
|
|
297
|
+
result[plugin_name] = stdout.rstrip
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
result
|
|
303
|
+
end
|
|
304
|
+
|
|
109
305
|
def self.extract_name_and_version(name)
|
|
110
306
|
split_name = name.split('-')
|
|
111
307
|
if split_name.length > 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|