openc3 7.3.0 → 7.4.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.
Files changed (499) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Guardfile +0 -0
  4. data/LICENSE.md +0 -0
  5. data/README.md +0 -0
  6. data/Rakefile +0 -0
  7. data/bin/openc3cli +139 -0
  8. data/bin/pipuninstall +0 -0
  9. data/bin/uvinstall +10 -3
  10. data/data/config/_array_params.yaml +0 -0
  11. data/data/config/_canvas_values.yaml +0 -0
  12. data/data/config/_graph_params.yaml +0 -0
  13. data/data/config/_id_items.yaml +0 -0
  14. data/data/config/_id_params.yaml +0 -0
  15. data/data/config/_interfaces.yaml +0 -0
  16. data/data/config/_items.yaml +0 -0
  17. data/data/config/_params.yaml +0 -0
  18. data/data/config/command.yaml +0 -0
  19. data/data/config/command_modifiers.yaml +2 -0
  20. data/data/config/command_telemetry.yaml +0 -0
  21. data/data/config/conversions.yaml +0 -0
  22. data/data/config/graph_settings.yaml +0 -0
  23. data/data/config/interface_modifiers.yaml +103 -1
  24. data/data/config/item_modifiers.yaml +0 -0
  25. data/data/config/microservice.yaml +5 -2
  26. data/data/config/param_item_modifiers.yaml +0 -0
  27. data/data/config/parameter_modifiers.yaml +0 -0
  28. data/data/config/plugins.yaml +0 -0
  29. data/data/config/processors.yaml +0 -0
  30. data/data/config/protocols.yaml +0 -0
  31. data/data/config/screen.yaml +0 -0
  32. data/data/config/settings.yaml +0 -0
  33. data/data/config/table_manager.yaml +0 -0
  34. data/data/config/table_parameter_modifiers.yaml +0 -0
  35. data/data/config/target.yaml +0 -0
  36. data/data/config/target_config.yaml +0 -0
  37. data/data/config/telemetry.yaml +0 -0
  38. data/data/config/telemetry_modifiers.yaml +2 -0
  39. data/data/config/tool.yaml +0 -0
  40. data/data/config/unknown.yaml +0 -0
  41. data/data/config/widgets.yaml +6 -0
  42. data/data/xtce_schemas/SpaceSystem_20180204.xsd +0 -0
  43. data/data/xtce_schemas/xml.xsd +0 -0
  44. data/ext/openc3/ext/array/array.c +0 -0
  45. data/ext/openc3/ext/array/extconf.rb +0 -0
  46. data/ext/openc3/ext/buffered_file/buffered_file.c +0 -0
  47. data/ext/openc3/ext/buffered_file/extconf.rb +0 -0
  48. data/ext/openc3/ext/burst_protocol/burst_protocol.c +6 -0
  49. data/ext/openc3/ext/burst_protocol/extconf.rb +0 -0
  50. data/ext/openc3/ext/config_parser/config_parser.c +0 -0
  51. data/ext/openc3/ext/config_parser/extconf.rb +0 -0
  52. data/ext/openc3/ext/crc/crc.c +0 -0
  53. data/ext/openc3/ext/crc/extconf.rb +0 -0
  54. data/ext/openc3/ext/openc3_io/extconf.rb +0 -0
  55. data/ext/openc3/ext/openc3_io/openc3_io.c +0 -0
  56. data/ext/openc3/ext/packet/extconf.rb +0 -0
  57. data/ext/openc3/ext/packet/packet.c +0 -0
  58. data/ext/openc3/ext/platform/extconf.rb +0 -0
  59. data/ext/openc3/ext/platform/platform.c +4 -1
  60. data/ext/openc3/ext/polynomial_conversion/extconf.rb +0 -0
  61. data/ext/openc3/ext/polynomial_conversion/polynomial_conversion.c +0 -0
  62. data/ext/openc3/ext/string/extconf.rb +0 -0
  63. data/ext/openc3/ext/string/string.c +0 -0
  64. data/ext/openc3/ext/structure/structure.c +49 -17
  65. data/ext/openc3/ext/tabbed_plots_config/extconf.rb +0 -0
  66. data/ext/openc3/ext/tabbed_plots_config/tabbed_plots_config.c +0 -0
  67. data/ext/openc3/ext/telemetry/extconf.rb +0 -0
  68. data/ext/openc3/ext/telemetry/telemetry.c +0 -0
  69. data/lib/cosmos.rb +0 -0
  70. data/lib/cosmosc2.rb +0 -0
  71. data/lib/openc3/accessors/accessor.rb +0 -0
  72. data/lib/openc3/accessors/binary_accessor.rb +63 -9
  73. data/lib/openc3/accessors/cbor_accessor.rb +0 -0
  74. data/lib/openc3/accessors/form_accessor.rb +0 -0
  75. data/lib/openc3/accessors/html_accessor.rb +0 -0
  76. data/lib/openc3/accessors/http_accessor.rb +0 -0
  77. data/lib/openc3/accessors/json_accessor.rb +0 -0
  78. data/lib/openc3/accessors/template_accessor.rb +0 -0
  79. data/lib/openc3/accessors/xml_accessor.rb +0 -0
  80. data/lib/openc3/accessors.rb +0 -0
  81. data/lib/openc3/api/README.md +0 -0
  82. data/lib/openc3/api/api.rb +0 -0
  83. data/lib/openc3/api/authorized_api.rb +0 -0
  84. data/lib/openc3/api/calendar_api.rb +0 -0
  85. data/lib/openc3/api/cmd_api.rb +1 -1
  86. data/lib/openc3/api/config_api.rb +0 -0
  87. data/lib/openc3/api/interface_api.rb +0 -0
  88. data/lib/openc3/api/limits_api.rb +0 -0
  89. data/lib/openc3/api/metrics_api.rb +1 -1
  90. data/lib/openc3/api/offline_access_api.rb +1 -1
  91. data/lib/openc3/api/router_api.rb +0 -0
  92. data/lib/openc3/api/settings_api.rb +22 -18
  93. data/lib/openc3/api/stash_api.rb +1 -1
  94. data/lib/openc3/api/target_api.rb +0 -0
  95. data/lib/openc3/api/tlm_api.rb +19 -14
  96. data/lib/openc3/bridge/bridge.rb +0 -0
  97. data/lib/openc3/bridge/bridge_config.rb +0 -0
  98. data/lib/openc3/bridge/bridge_interface_thread.rb +0 -0
  99. data/lib/openc3/bridge/bridge_router_thread.rb +0 -0
  100. data/lib/openc3/ccsds/ccsds_packet.rb +0 -0
  101. data/lib/openc3/ccsds/ccsds_parser.rb +0 -0
  102. data/lib/openc3/config/config_parser.rb +78 -17
  103. data/lib/openc3/config/meta_config_parser.rb +0 -0
  104. data/lib/openc3/conversions/bit_reverse_conversion.rb +0 -0
  105. data/lib/openc3/conversions/conversion.rb +0 -0
  106. data/lib/openc3/conversions/generic_conversion.rb +0 -0
  107. data/lib/openc3/conversions/ip_read_conversion.rb +0 -0
  108. data/lib/openc3/conversions/ip_write_conversion.rb +0 -0
  109. data/lib/openc3/conversions/object_read_conversion.rb +0 -0
  110. data/lib/openc3/conversions/object_write_conversion.rb +0 -0
  111. data/lib/openc3/conversions/packet_time_formatted_conversion.rb +0 -0
  112. data/lib/openc3/conversions/packet_time_seconds_conversion.rb +0 -0
  113. data/lib/openc3/conversions/polynomial_conversion.rb +0 -0
  114. data/lib/openc3/conversions/processor_conversion.rb +0 -0
  115. data/lib/openc3/conversions/received_count_conversion.rb +0 -0
  116. data/lib/openc3/conversions/received_time_formatted_conversion.rb +0 -0
  117. data/lib/openc3/conversions/received_time_seconds_conversion.rb +0 -0
  118. data/lib/openc3/conversions/segmented_polynomial_conversion.rb +0 -0
  119. data/lib/openc3/conversions/unix_time_conversion.rb +0 -0
  120. data/lib/openc3/conversions/unix_time_formatted_conversion.rb +0 -0
  121. data/lib/openc3/conversions/unix_time_seconds_conversion.rb +0 -0
  122. data/lib/openc3/conversions.rb +0 -0
  123. data/lib/openc3/core_ext/array.rb +0 -0
  124. data/lib/openc3/core_ext/binding.rb +0 -0
  125. data/lib/openc3/core_ext/class.rb +0 -0
  126. data/lib/openc3/core_ext/exception.rb +0 -0
  127. data/lib/openc3/core_ext/faraday.rb +0 -0
  128. data/lib/openc3/core_ext/file.rb +0 -0
  129. data/lib/openc3/core_ext/io.rb +0 -0
  130. data/lib/openc3/core_ext/kernel.rb +0 -0
  131. data/lib/openc3/core_ext/math.rb +0 -0
  132. data/lib/openc3/core_ext/matrix.rb +0 -0
  133. data/lib/openc3/core_ext/objectspace.rb +0 -0
  134. data/lib/openc3/core_ext/openc3_io.rb +0 -0
  135. data/lib/openc3/core_ext/range.rb +0 -0
  136. data/lib/openc3/core_ext/socket.rb +0 -0
  137. data/lib/openc3/core_ext/string.rb +0 -0
  138. data/lib/openc3/core_ext/stringio.rb +0 -0
  139. data/lib/openc3/core_ext/tempfile.rb +0 -0
  140. data/lib/openc3/core_ext/time.rb +0 -0
  141. data/lib/openc3/core_ext.rb +0 -0
  142. data/lib/openc3/ext/.keep +0 -0
  143. data/lib/openc3/interfaces/file_interface.rb +0 -0
  144. data/lib/openc3/interfaces/http_client_interface.rb +0 -0
  145. data/lib/openc3/interfaces/http_server_interface.rb +0 -0
  146. data/lib/openc3/interfaces/interface.rb +5 -4
  147. data/lib/openc3/interfaces/mqtt_interface.rb +0 -0
  148. data/lib/openc3/interfaces/mqtt_stream_interface.rb +2 -0
  149. data/lib/openc3/interfaces/protocols/burst_protocol.rb +27 -0
  150. data/lib/openc3/interfaces/protocols/cmd_response_protocol.rb +0 -0
  151. data/lib/openc3/interfaces/protocols/cobs_protocol.rb +0 -0
  152. data/lib/openc3/interfaces/protocols/crc_protocol.rb +0 -0
  153. data/lib/openc3/interfaces/protocols/fixed_protocol.rb +0 -0
  154. data/lib/openc3/interfaces/protocols/ignore_packet_protocol.rb +0 -0
  155. data/lib/openc3/interfaces/protocols/length_protocol.rb +7 -0
  156. data/lib/openc3/interfaces/protocols/preidentified_protocol.rb +7 -0
  157. data/lib/openc3/interfaces/protocols/protocol.rb +12 -4
  158. data/lib/openc3/interfaces/protocols/slip_protocol.rb +0 -0
  159. data/lib/openc3/interfaces/protocols/template_protocol.rb +2 -2
  160. data/lib/openc3/interfaces/protocols/terminated_protocol.rb +0 -0
  161. data/lib/openc3/interfaces/serial_interface.rb +2 -0
  162. data/lib/openc3/interfaces/simulated_target_interface.rb +0 -0
  163. data/lib/openc3/interfaces/stream_interface.rb +0 -0
  164. data/lib/openc3/interfaces/tcpip_client_interface.rb +2 -0
  165. data/lib/openc3/interfaces/tcpip_server_interface.rb +0 -0
  166. data/lib/openc3/interfaces/udp_interface.rb +16 -5
  167. data/lib/openc3/interfaces.rb +0 -0
  168. data/lib/openc3/io/buffered_file.rb +0 -0
  169. data/lib/openc3/io/io_multiplexer.rb +0 -0
  170. data/lib/openc3/io/json_api.rb +3 -2
  171. data/lib/openc3/io/json_api_object.rb +24 -3
  172. data/lib/openc3/io/json_drb.rb +20 -4
  173. data/lib/openc3/io/json_drb_object.rb +3 -2
  174. data/lib/openc3/io/json_drb_rack.rb +0 -0
  175. data/lib/openc3/io/json_rpc.rb +0 -0
  176. data/lib/openc3/io/posix_serial_driver.rb +0 -0
  177. data/lib/openc3/io/serial_driver.rb +0 -0
  178. data/lib/openc3/io/stderr.rb +0 -0
  179. data/lib/openc3/io/stdout.rb +0 -0
  180. data/lib/openc3/io/udp_sockets.rb +38 -8
  181. data/lib/openc3/io/win32_serial_driver.rb +1 -0
  182. data/lib/openc3/logs/buffered_packet_log_reader.rb +0 -0
  183. data/lib/openc3/logs/buffered_packet_log_writer.rb +0 -0
  184. data/lib/openc3/logs/log_writer.rb +0 -0
  185. data/lib/openc3/logs/packet_log_constants.rb +0 -0
  186. data/lib/openc3/logs/packet_log_reader.rb +23 -2
  187. data/lib/openc3/logs/packet_log_writer.rb +5 -0
  188. data/lib/openc3/logs/stream_log.rb +1 -1
  189. data/lib/openc3/logs/stream_log_pair.rb +0 -0
  190. data/lib/openc3/logs/text_log_writer.rb +0 -0
  191. data/lib/openc3/logs.rb +0 -0
  192. data/lib/openc3/microservices/cleanup_microservice.rb +0 -0
  193. data/lib/openc3/microservices/decom_common.rb +0 -0
  194. data/lib/openc3/microservices/decom_microservice.rb +0 -0
  195. data/lib/openc3/microservices/interface_decom_common.rb +2 -1
  196. data/lib/openc3/microservices/interface_microservice.rb +72 -25
  197. data/lib/openc3/microservices/log_microservice.rb +0 -0
  198. data/lib/openc3/microservices/microservice.rb +3 -3
  199. data/lib/openc3/microservices/multi_microservice.rb +1 -1
  200. data/lib/openc3/microservices/periodic_microservice.rb +2 -2
  201. data/lib/openc3/microservices/plugin_microservice.rb +0 -0
  202. data/lib/openc3/microservices/queue_microservice.rb +0 -0
  203. data/lib/openc3/microservices/router_microservice.rb +0 -0
  204. data/lib/openc3/microservices/scope_cleanup_microservice.rb +0 -0
  205. data/lib/openc3/microservices/text_log_microservice.rb +0 -0
  206. data/lib/openc3/migrations/20220420190000_log_stuff.rb +0 -0
  207. data/lib/openc3/migrations/20221202214600_add_target_names.rb +0 -0
  208. data/lib/openc3/migrations/20221210174900_convert_to_multi.rb +0 -0
  209. data/lib/openc3/migrations/20230915000002_no_scope_log_messages.rb +0 -0
  210. data/lib/openc3/migrations/20231022000000_tlm_viewer_config.rb +0 -0
  211. data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +0 -0
  212. data/lib/openc3/migrations/20241208080001_no_trigger_group.rb +0 -0
  213. data/lib/openc3/migrations/20250108060000_news_feed.rb +0 -0
  214. data/lib/openc3/migrations/20250402000000_periodic_only_default.rb +0 -0
  215. data/lib/openc3/migrations/20260203000000_remove_store_id.rb +0 -0
  216. data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +0 -0
  217. data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +0 -0
  218. data/lib/openc3/migrations/20260704000000_default_bridge.rb +38 -0
  219. data/lib/openc3/migrations/20260731000000_system_health_json.rb +11 -0
  220. data/lib/openc3/models/activity_model.rb +0 -0
  221. data/lib/openc3/models/auth_model.rb +35 -7
  222. data/lib/openc3/models/bridge_model.rb +133 -0
  223. data/lib/openc3/models/config_keyword_model.rb +63 -0
  224. data/lib/openc3/models/cvt_model.rb +0 -0
  225. data/lib/openc3/models/db_sharded_model.rb +0 -0
  226. data/lib/openc3/models/environment_model.rb +0 -0
  227. data/lib/openc3/models/gem_model.rb +12 -9
  228. data/lib/openc3/models/host_interface_microservice_model.rb +114 -0
  229. data/lib/openc3/models/info_model.rb +0 -0
  230. data/lib/openc3/models/interface_model.rb +178 -19
  231. data/lib/openc3/models/interface_status_model.rb +0 -0
  232. data/lib/openc3/models/metadata_model.rb +0 -0
  233. data/lib/openc3/models/metric_model.rb +0 -0
  234. data/lib/openc3/models/microservice_model.rb +9 -22
  235. data/lib/openc3/models/microservice_status_model.rb +0 -0
  236. data/lib/openc3/models/migration_model.rb +0 -0
  237. data/lib/openc3/models/model.rb +0 -0
  238. data/lib/openc3/models/news_model.rb +0 -0
  239. data/lib/openc3/models/note_model.rb +0 -0
  240. data/lib/openc3/models/offline_access_model.rb +0 -0
  241. data/lib/openc3/models/ping_model.rb +0 -0
  242. data/lib/openc3/models/plugin_model.rb +9 -2
  243. data/lib/openc3/models/plugin_store_model.rb +0 -0
  244. data/lib/openc3/models/process_status_model.rb +0 -0
  245. data/lib/openc3/models/python_package_model.rb +4 -4
  246. data/lib/openc3/models/queue_model.rb +0 -0
  247. data/lib/openc3/models/reaction_model.rb +6 -3
  248. data/lib/openc3/models/reingest_job_model.rb +0 -0
  249. data/lib/openc3/models/router_model.rb +0 -0
  250. data/lib/openc3/models/router_status_model.rb +0 -0
  251. data/lib/openc3/models/scope_model.rb +51 -26
  252. data/lib/openc3/models/script_engine_model.rb +0 -0
  253. data/lib/openc3/models/script_status_model.rb +0 -0
  254. data/lib/openc3/models/secret_model.rb +0 -0
  255. data/lib/openc3/models/setting_model.rb +548 -3
  256. data/lib/openc3/models/sorted_model.rb +0 -0
  257. data/lib/openc3/models/stash_model.rb +0 -0
  258. data/lib/openc3/models/target_model.rb +30 -25
  259. data/lib/openc3/models/timeline_model.rb +0 -0
  260. data/lib/openc3/models/tool_config_model.rb +0 -0
  261. data/lib/openc3/models/tool_model.rb +1 -0
  262. data/lib/openc3/models/trigger_group_model.rb +0 -0
  263. data/lib/openc3/models/trigger_model.rb +20 -0
  264. data/lib/openc3/models/widget_model.rb +0 -0
  265. data/lib/openc3/operators/microservice_operator.rb +64 -22
  266. data/lib/openc3/operators/operator.rb +4 -4
  267. data/lib/openc3/packets/binary_accessor.rb +0 -0
  268. data/lib/openc3/packets/command_validator.rb +0 -0
  269. data/lib/openc3/packets/commands.rb +0 -0
  270. data/lib/openc3/packets/json_packet.rb +0 -0
  271. data/lib/openc3/packets/limits.rb +0 -0
  272. data/lib/openc3/packets/limits_response.rb +0 -0
  273. data/lib/openc3/packets/packet.rb +4 -0
  274. data/lib/openc3/packets/packet_config.rb +36 -1
  275. data/lib/openc3/packets/packet_item.rb +0 -0
  276. data/lib/openc3/packets/packet_item_limits.rb +0 -0
  277. data/lib/openc3/packets/parsers/format_string_parser.rb +0 -0
  278. data/lib/openc3/packets/parsers/limits_parser.rb +0 -0
  279. data/lib/openc3/packets/parsers/limits_response_parser.rb +0 -0
  280. data/lib/openc3/packets/parsers/packet_item_parser.rb +0 -0
  281. data/lib/openc3/packets/parsers/packet_parser.rb +0 -0
  282. data/lib/openc3/packets/parsers/processor_parser.rb +0 -0
  283. data/lib/openc3/packets/parsers/state_parser.rb +0 -0
  284. data/lib/openc3/packets/parsers/xtce_converter.rb +0 -0
  285. data/lib/openc3/packets/parsers/xtce_parser.rb +8 -1
  286. data/lib/openc3/packets/structure.rb +7 -1
  287. data/lib/openc3/packets/structure_item.rb +0 -0
  288. data/lib/openc3/packets/telemetry.rb +0 -0
  289. data/lib/openc3/processors/processor.rb +2 -0
  290. data/lib/openc3/processors/statistics_processor.rb +0 -0
  291. data/lib/openc3/processors/watermark_processor.rb +0 -0
  292. data/lib/openc3/processors.rb +0 -0
  293. data/lib/openc3/script/api_shared.rb +0 -0
  294. data/lib/openc3/script/autonomic.rb +12 -12
  295. data/lib/openc3/script/calendar.rb +0 -0
  296. data/lib/openc3/script/commands.rb +0 -0
  297. data/lib/openc3/script/critical_cmd.rb +0 -0
  298. data/lib/openc3/script/exceptions.rb +0 -0
  299. data/lib/openc3/script/extract.rb +0 -0
  300. data/lib/openc3/script/limits.rb +0 -0
  301. data/lib/openc3/script/metadata.rb +0 -0
  302. data/lib/openc3/script/packages.rb +0 -0
  303. data/lib/openc3/script/plugins.rb +0 -0
  304. data/lib/openc3/script/queue.rb +0 -0
  305. data/lib/openc3/script/screen.rb +0 -0
  306. data/lib/openc3/script/script.rb +2 -2
  307. data/lib/openc3/script/script_runner.rb +0 -0
  308. data/lib/openc3/script/storage.rb +4 -4
  309. data/lib/openc3/script/suite.rb +0 -0
  310. data/lib/openc3/script/suite_results.rb +0 -0
  311. data/lib/openc3/script/suite_runner.rb +26 -0
  312. data/lib/openc3/script/tables.rb +5 -1
  313. data/lib/openc3/script/telemetry.rb +9 -3
  314. data/lib/openc3/script/web_socket_api.rb +218 -145
  315. data/lib/openc3/script.rb +0 -0
  316. data/lib/openc3/script_engines/script_engine.rb +0 -0
  317. data/lib/openc3/streams/mqtt_stream.rb +0 -0
  318. data/lib/openc3/streams/serial_stream.rb +0 -0
  319. data/lib/openc3/streams/stream.rb +0 -0
  320. data/lib/openc3/streams/tcpip_client_stream.rb +0 -0
  321. data/lib/openc3/streams/tcpip_socket_stream.rb +0 -0
  322. data/lib/openc3/streams/web_socket_client_stream.rb +0 -0
  323. data/lib/openc3/subpacketizers/subpacketizer.rb +0 -0
  324. data/lib/openc3/system/system.rb +0 -0
  325. data/lib/openc3/system/target.rb +0 -0
  326. data/lib/openc3/system.rb +0 -0
  327. data/lib/openc3/tools/cmd_tlm_server/api.rb +0 -0
  328. data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +7 -0
  329. data/lib/openc3/tools/table_manager/table.rb +0 -0
  330. data/lib/openc3/tools/table_manager/table_config.rb +0 -0
  331. data/lib/openc3/tools/table_manager/table_item.rb +0 -0
  332. data/lib/openc3/tools/table_manager/table_item_parser.rb +0 -0
  333. data/lib/openc3/tools/table_manager/table_manager_core.rb +0 -0
  334. data/lib/openc3/tools/table_manager/table_parser.rb +0 -0
  335. data/lib/openc3/tools/test_runner/test.rb +0 -0
  336. data/lib/openc3/top_level.rb +1 -1
  337. data/lib/openc3/topics/autonomic_topic.rb +0 -0
  338. data/lib/openc3/topics/calendar_topic.rb +0 -0
  339. data/lib/openc3/topics/command_decom_topic.rb +0 -0
  340. data/lib/openc3/topics/command_topic.rb +0 -0
  341. data/lib/openc3/topics/config_topic.rb +0 -0
  342. data/lib/openc3/topics/decom_interface_topic.rb +2 -1
  343. data/lib/openc3/topics/interface_topic.rb +8 -2
  344. data/lib/openc3/topics/limits_event_topic.rb +0 -0
  345. data/lib/openc3/topics/notebook_topic.rb +0 -0
  346. data/lib/openc3/topics/queue_topic.rb +0 -0
  347. data/lib/openc3/topics/router_topic.rb +6 -1
  348. data/lib/openc3/topics/system_events_topic.rb +0 -0
  349. data/lib/openc3/topics/telemetry_decom_topic.rb +0 -0
  350. data/lib/openc3/topics/telemetry_topic.rb +0 -0
  351. data/lib/openc3/topics/timeline_topic.rb +0 -0
  352. data/lib/openc3/topics/topic.rb +0 -0
  353. data/lib/openc3/utilities/authentication.rb +4 -4
  354. data/lib/openc3/utilities/authorization.rb +0 -0
  355. data/lib/openc3/utilities/aws_bucket.rb +2 -2
  356. data/lib/openc3/utilities/bucket.rb +1 -1
  357. data/lib/openc3/utilities/bucket_file_cache.rb +55 -22
  358. data/lib/openc3/utilities/bucket_require.rb +0 -0
  359. data/lib/openc3/utilities/bucket_utilities.rb +3 -4
  360. data/lib/openc3/utilities/cli_generator.rb +0 -0
  361. data/lib/openc3/utilities/cmd_log.rb +0 -0
  362. data/lib/openc3/utilities/config_overlay.rb +75 -0
  363. data/lib/openc3/utilities/cosmos_rails_formatter.rb +0 -0
  364. data/lib/openc3/utilities/crc.rb +59 -32
  365. data/lib/openc3/utilities/csv.rb +0 -0
  366. data/lib/openc3/utilities/ctrf.rb +0 -0
  367. data/lib/openc3/utilities/env_helper.rb +0 -0
  368. data/lib/openc3/utilities/local_bucket.rb +0 -0
  369. data/lib/openc3/utilities/local_mode.rb +32 -6
  370. data/lib/openc3/utilities/logger.rb +30 -8
  371. data/lib/openc3/utilities/message_log.rb +0 -0
  372. data/lib/openc3/utilities/metric.rb +0 -0
  373. data/lib/openc3/utilities/migration.rb +0 -0
  374. data/lib/openc3/utilities/open_telemetry.rb +0 -0
  375. data/lib/openc3/utilities/process_manager.rb +0 -0
  376. data/lib/openc3/utilities/pypi_url.rb +0 -0
  377. data/lib/openc3/utilities/python_proxy.rb +0 -0
  378. data/lib/openc3/utilities/python_venv.rb +135 -0
  379. data/lib/openc3/utilities/quaternion.rb +0 -0
  380. data/lib/openc3/utilities/questdb_client.rb +29 -46
  381. data/lib/openc3/utilities/redis_secrets.rb +0 -0
  382. data/lib/openc3/utilities/reingest_job.rb +0 -0
  383. data/lib/openc3/utilities/ruby_lex_utils.rb +0 -0
  384. data/lib/openc3/utilities/rubygems_url.rb +39 -0
  385. data/lib/openc3/utilities/running_script.rb +24 -55
  386. data/lib/openc3/utilities/s3_autoload.rb +5 -5
  387. data/lib/openc3/utilities/script.rb +12 -8
  388. data/lib/openc3/utilities/secrets.rb +48 -3
  389. data/lib/openc3/utilities/simulated_target.rb +0 -0
  390. data/lib/openc3/utilities/sleeper.rb +0 -0
  391. data/lib/openc3/utilities/store.rb +0 -0
  392. data/lib/openc3/utilities/store_autoload.rb +2 -2
  393. data/lib/openc3/utilities/store_queued.rb +0 -0
  394. data/lib/openc3/utilities/target_file.rb +18 -1
  395. data/lib/openc3/utilities/thread_manager.rb +0 -0
  396. data/lib/openc3/utilities/throttle.rb +0 -0
  397. data/lib/openc3/utilities/zip.rb +0 -0
  398. data/lib/openc3/utilities.rb +0 -0
  399. data/lib/openc3/version.rb +6 -6
  400. data/lib/openc3/win32/excel.rb +0 -0
  401. data/lib/openc3/win32/win32.rb +0 -0
  402. data/lib/openc3/win32/win32_main.rb +0 -0
  403. data/lib/openc3.rb +2 -2
  404. data/tasks/gemfile_stats.rake +0 -0
  405. data/tasks/spec.rake +0 -0
  406. data/templates/command_validator/command_validator.py +0 -0
  407. data/templates/command_validator/command_validator.rb +0 -0
  408. data/templates/conversion/conversion.py +0 -0
  409. data/templates/conversion/conversion.rb +0 -0
  410. data/templates/limits_response/response.py +0 -0
  411. data/templates/limits_response/response.rb +0 -0
  412. data/templates/microservice/microservices/TEMPLATE/microservice.py +0 -0
  413. data/templates/microservice/microservices/TEMPLATE/microservice.rb +0 -0
  414. data/templates/plugin/.gitignore +0 -0
  415. data/templates/plugin/LICENSE.md +0 -0
  416. data/templates/plugin/README.md +0 -0
  417. data/templates/plugin/Rakefile +0 -0
  418. data/templates/plugin/plugin.gemspec +0 -0
  419. data/templates/plugin/plugin.txt +0 -0
  420. data/templates/processor/processor.py +1 -1
  421. data/templates/processor/processor.rb +0 -0
  422. data/templates/target/targets/TARGET/cmd_tlm/cmd.txt +0 -0
  423. data/templates/target/targets/TARGET/cmd_tlm/tlm.txt +0 -0
  424. data/templates/target/targets/TARGET/lib/target.py +0 -0
  425. data/templates/target/targets/TARGET/lib/target.rb +0 -0
  426. data/templates/target/targets/TARGET/procedures/procedure.py +0 -0
  427. data/templates/target/targets/TARGET/procedures/procedure.rb +0 -0
  428. data/templates/target/targets/TARGET/public/README.txt +0 -0
  429. data/templates/target/targets/TARGET/screens/status.txt +0 -0
  430. data/templates/target/targets/TARGET/target.txt +0 -0
  431. data/templates/tool_angular/.editorconfig +0 -0
  432. data/templates/tool_angular/.gitignore +0 -0
  433. data/templates/tool_angular/.vscode/extensions.json +0 -0
  434. data/templates/tool_angular/.vscode/launch.json +0 -0
  435. data/templates/tool_angular/.vscode/tasks.json +0 -0
  436. data/templates/tool_angular/angular.json +0 -0
  437. data/templates/tool_angular/extra-webpack.config.js +0 -0
  438. data/templates/tool_angular/package.json +2 -2
  439. data/templates/tool_angular/src/app/app-routing.module.ts +0 -0
  440. data/templates/tool_angular/src/app/app.component.html +0 -0
  441. data/templates/tool_angular/src/app/app.component.scss +0 -0
  442. data/templates/tool_angular/src/app/app.component.spec.ts +0 -0
  443. data/templates/tool_angular/src/app/app.component.ts +0 -0
  444. data/templates/tool_angular/src/app/app.module.ts +0 -0
  445. data/templates/tool_angular/src/app/custom-overlay-container.ts +0 -0
  446. data/templates/tool_angular/src/app/empty-route/empty-route.component.ts +0 -0
  447. data/templates/tool_angular/src/app/openc3-api.d.ts +0 -0
  448. data/templates/tool_angular/src/assets/.gitkeep +0 -0
  449. data/templates/tool_angular/src/environments/environment.prod.ts +0 -0
  450. data/templates/tool_angular/src/environments/environment.ts +0 -0
  451. data/templates/tool_angular/src/favicon.ico +0 -0
  452. data/templates/tool_angular/src/index.html +0 -0
  453. data/templates/tool_angular/src/main.single-spa.ts +0 -0
  454. data/templates/tool_angular/src/single-spa/asset-url.ts +0 -0
  455. data/templates/tool_angular/src/single-spa/single-spa-props.ts +0 -0
  456. data/templates/tool_angular/src/styles.scss +0 -0
  457. data/templates/tool_angular/tsconfig.app.json +0 -0
  458. data/templates/tool_angular/tsconfig.json +0 -0
  459. data/templates/tool_angular/tsconfig.spec.json +0 -0
  460. data/templates/tool_react/.eslintrc +0 -0
  461. data/templates/tool_react/.gitignore +0 -0
  462. data/templates/tool_react/.prettierignore +0 -0
  463. data/templates/tool_react/babel.config.json +0 -0
  464. data/templates/tool_react/jest.config.js +0 -0
  465. data/templates/tool_react/package.json +1 -1
  466. data/templates/tool_react/src/openc3-tool_name.js +0 -0
  467. data/templates/tool_react/src/root.component.js +0 -0
  468. data/templates/tool_react/src/root.component.test.js +0 -0
  469. data/templates/tool_react/webpack.config.js +0 -0
  470. data/templates/tool_svelte/.gitignore +0 -0
  471. data/templates/tool_svelte/.prettierignore +0 -0
  472. data/templates/tool_svelte/.prettierrc.js +0 -0
  473. data/templates/tool_svelte/babel.config.js +0 -0
  474. data/templates/tool_svelte/build/smui.css +0 -0
  475. data/templates/tool_svelte/jest.config.js +0 -0
  476. data/templates/tool_svelte/package.json +1 -1
  477. data/templates/tool_svelte/rollup.config.js +0 -0
  478. data/templates/tool_svelte/src/App.svelte +0 -0
  479. data/templates/tool_svelte/src/App.test.js +0 -0
  480. data/templates/tool_svelte/src/theme/_smui-theme.scss +0 -0
  481. data/templates/tool_svelte/src/tool_name.js +0 -0
  482. data/templates/tool_vue/.env.standalone +0 -0
  483. data/templates/tool_vue/.gitignore +0 -0
  484. data/templates/tool_vue/.prettierrc.cjs +0 -0
  485. data/templates/tool_vue/eslint.config.mjs +0 -0
  486. data/templates/tool_vue/jsconfig.json +0 -0
  487. data/templates/tool_vue/package.json +3 -3
  488. data/templates/tool_vue/src/App.vue +0 -0
  489. data/templates/tool_vue/src/main.js +0 -0
  490. data/templates/tool_vue/src/router.js +0 -0
  491. data/templates/tool_vue/src/tools/tool_name/tool_name.vue +0 -0
  492. data/templates/tool_vue/vite.config.js +1 -1
  493. data/templates/widget/.prettierrc.cjs +0 -0
  494. data/templates/widget/LICENSE.md +0 -0
  495. data/templates/widget/Rakefile +0 -0
  496. data/templates/widget/package.json +2 -2
  497. data/templates/widget/src/Widget.vue +0 -0
  498. data/templates/widget/vite.config.js +0 -0
  499. metadata +56 -9
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,135 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2026 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is distributed in the hope that it will be useful,
7
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ # See LICENSE.md for more details.
10
+ #
11
+ # This file may also be used under the terms of a commercial license
12
+ # if purchased from OpenC3, Inc.
13
+
14
+ require 'open3'
15
+ require 'openc3/models/target_model'
16
+ require 'openc3/utilities/target_file'
17
+
18
+ module OpenC3
19
+ module PythonVenv
20
+ PLUGIN_VENVS_DIR = '/gems/plugin_venvs'
21
+
22
+ # Shared user site-packages directory used by plugins that predate the
23
+ # per-plugin UV venvs. Every caller that falls back off a plugin venv points
24
+ # PYTHONUSERBASE here, so keep it in one place rather than repeating the
25
+ # literal in script.rb, running_script.rb, microservice_operator.rb and
26
+ # microservice_model.rb.
27
+ DEFAULT_PYTHONUSERBASE = '/gems/python_packages'
28
+
29
+ # Packages a plugin venv must never supply itself. The scripts and
30
+ # microservices that use a plugin venv still run the base venv's Python
31
+ # binary, and CPython searches every PYTHONPATH entry ahead of the running
32
+ # venv's own site-packages. A plugin that declares openc3 as a dependency
33
+ # would therefore shadow the system library with its own copy, silently
34
+ # swapping the API client the script talks to COSMOS with. Reordering
35
+ # PYTHONPATH cannot fix this - the whole variable outranks the base venv -
36
+ # so the copy is removed at install time instead.
37
+ RESERVED_PACKAGES = ['openc3'].freeze
38
+
39
+ # Resolve a script's plugin venv and expose its packages to a child process.
40
+ # Returns the venv path when configured, or nil when the system environment
41
+ # should be used.
42
+ def self.configure_for_script(environment, name:, scope:, python_venv: nil)
43
+ venv_dir = resolve_script_venv(name: name, scope: scope, python_venv: python_venv)
44
+ return nil unless venv_dir
45
+
46
+ configure_environment(environment, venv_dir)
47
+ venv_dir
48
+ end
49
+
50
+ def self.configure_environment(environment, venv_dir)
51
+ environment['VIRTUAL_ENV'] = venv_dir
52
+ environment['PATH'] = "#{venv_dir}/bin:#{ENV.fetch('PATH', '')}"
53
+ environment['PYTHONUSERBASE'] = venv_dir
54
+
55
+ site_packages = Dir.glob("#{venv_dir}/lib/python*/site-packages").first
56
+ # Prefer a PYTHONPATH the caller already seeded on `environment` over the
57
+ # ambient one. Script.process_suite sets it to ENV['PYTHONPATH'] || '.'
58
+ # before calling here, and reading ENV directly would drop that value.
59
+ existing_pythonpath = environment['PYTHONPATH'] || ENV.fetch('PYTHONPATH', '')
60
+ if site_packages
61
+ environment['PYTHONPATH'] = existing_pythonpath.empty? ? site_packages : "#{site_packages}:#{existing_pythonpath}"
62
+ else
63
+ environment['PYTHONPATH'] = existing_pythonpath.empty? ? nil : existing_pythonpath
64
+ end
65
+
66
+ environment
67
+ end
68
+
69
+ # Remove any RESERVED_PACKAGES a plugin installed into its own venv.
70
+ # Returns the names that were removed so callers can report them.
71
+ def self.purge_reserved_packages(venv_dir)
72
+ return [] if venv_dir.nil? || venv_dir.empty?
73
+
74
+ purged = []
75
+ RESERVED_PACKAGES.each do |package|
76
+ next unless package_installed?(venv_dir, package)
77
+
78
+ Logger.warn("Plugin venv #{venv_dir} declares '#{package}', which would shadow the " \
79
+ "system library. Removing it; the plugin will use the system #{package}.")
80
+ _output, status = Open3.capture2e('uv', 'pip', 'uninstall', '--python', venv_dir, package)
81
+ if status.success?
82
+ purged << package
83
+ else
84
+ Logger.error("Failed to remove '#{package}' from plugin venv #{venv_dir}")
85
+ end
86
+ end
87
+ purged
88
+ rescue => e
89
+ Logger.error("Could not purge reserved packages from #{venv_dir}: #{e.message}")
90
+ []
91
+ end
92
+
93
+ def self.package_installed?(venv_dir, package)
94
+ _output, status = Open3.capture2e('uv', 'pip', 'show', '--python', venv_dir, package)
95
+ status.success?
96
+ end
97
+ private_class_method :package_installed?
98
+
99
+ def self.plugin_venv_path(scope:, plugin_name:)
100
+ sanitized_name = "#{scope}__#{plugin_name}".tr('^a-zA-Z0-9_-', '_')
101
+ candidate = File.join(PLUGIN_VENVS_DIR, sanitized_name, '.venv')
102
+ candidate if File.directory?(candidate)
103
+ end
104
+
105
+ def self.resolve_script_venv(name:, scope:, python_venv: nil)
106
+ target_name = name.split('/')[0].to_s.upcase
107
+ if target_name == TargetFile::TEMP_FOLDER && python_venv
108
+ # File.basename strips directory components; the tr() then whitelists the
109
+ # name down to the character set PluginModel.plugin_venv_name builds it
110
+ # from. That leaves no path separators and no glob metacharacters, so
111
+ # the value cannot escape PLUGIN_VENVS_DIR or widen the Dir.glob in
112
+ # configure_environment.
113
+ safe_name = File.basename(python_venv.to_s).tr('^a-zA-Z0-9_-', '_')
114
+ # The venv name is client-supplied, so it must also be confined to the
115
+ # caller's scope. Venv directories are named "<scope>__<plugin>" by
116
+ # PluginModel.plugin_venv_name; without this check a user in one scope
117
+ # could name another scope's venv and run against its packages.
118
+ return nil unless safe_name.start_with?("#{scope}__".tr('^a-zA-Z0-9_-', '_'))
119
+
120
+ candidate = File.join(PLUGIN_VENVS_DIR, safe_name, '.venv')
121
+ return candidate if File.directory?(candidate)
122
+ else
123
+ target_info = TargetModel.get(name: target_name, scope: scope)
124
+ if target_info && target_info['plugin']
125
+ return plugin_venv_path(scope: scope, plugin_name: target_info['plugin'])
126
+ end
127
+ end
128
+ nil
129
+ rescue => e
130
+ Logger.debug("Could not resolve plugin venv for script '#{name}': #{e.message}")
131
+ nil
132
+ end
133
+ private_class_method :resolve_script_venv
134
+ end
135
+ end
File without changes
@@ -928,6 +928,10 @@ module OpenC3
928
928
  # @return [Array, Hash] Array of [value, limits_state] pairs per row, or {} if no results.
929
929
  # Single-row results return a flat array; multi-row results return array of arrays.
930
930
  def self.tsdb_lookup(items, start_time:, end_time: nil, scope: "DEFAULT")
931
+ # Every item is a placeholder for an item which doesn't exist, so there's
932
+ # nothing to query. Return a single row of nil values, one per item.
933
+ return Array.new(items.length) { [nil, nil] } if items.all? { |item| item[2].nil? }
934
+
931
935
  # Group items by db_shard number while preserving their original positions
932
936
  db_shard_groups = {} # db_shard => { positions: [], items: [] }
933
937
  items.each_with_index do |item, pos|
@@ -944,62 +948,36 @@ module OpenC3
944
948
  return tsdb_lookup_single_db_shard(group[:items], start_time: start_time, end_time: end_time, scope: scope, db_shard: db_shard)
945
949
  end
946
950
 
947
- # Cross-db_shard: execute per-db_shard queries and merge results
948
- db_shard_results = {} # db_shard => data
951
+ # Cross-db_shard: execute per-db_shard queries (as arrays of rows) and merge results
952
+ db_shard_rows = {} # db_shard => rows
949
953
  db_shard_groups.each do |db_shard, group|
950
- result = tsdb_lookup_single_db_shard(group[:items], start_time: start_time, end_time: end_time, scope: scope, db_shard: db_shard)
951
- db_shard_results[db_shard] = result
954
+ result = tsdb_lookup_single_db_shard(group[:items], start_time: start_time, end_time: end_time, scope: scope, db_shard: db_shard, flatten: false)
955
+ db_shard_rows[db_shard] = result.is_a?(Array) ? result : []
952
956
  end
953
957
 
954
- # If all db_shards returned empty, return empty
955
- return {} if db_shard_results.values.all? { |r| r == {} }
956
-
957
- # Merge results positionally back into the original item order.
958
- # For single-row results (no end_time), merge flat arrays.
959
- # For multi-row results, each db_shard may have different row counts;
960
- # use the maximum row count and fill missing positions with [nil, nil].
961
- if !end_time
962
- # Single-row mode: each db_shard returns a flat array of [value, limits] pairs.
963
- # Merge them into the original item order.
964
- merged = Array.new(items.length) { [nil, nil] }
965
- db_shard_groups.each do |db_shard, group|
966
- result = db_shard_results[db_shard]
967
- next if result == {} || !result.is_a?(Array)
958
+ # Merge results positionally back into the original item order. Each db_shard
959
+ # may have different row counts so use the maximum row count and fill missing
960
+ # positions with [nil, nil]. If all db_shards returned empty, return empty.
961
+ max_rows = db_shard_rows.values.map(&:length).max
962
+ return {} if max_rows == 0
963
+
964
+ merged = Array.new(max_rows) { Array.new(items.length) { [nil, nil] } }
965
+ db_shard_groups.each do |db_shard, group|
966
+ db_shard_rows[db_shard].each_with_index do |row, row_num|
968
967
  group[:positions].each_with_index do |orig_pos, db_shard_idx|
969
- merged[orig_pos] = result[db_shard_idx] if result[db_shard_idx]
968
+ merged[row_num][orig_pos] = row[db_shard_idx] if row[db_shard_idx]
970
969
  end
971
970
  end
972
- merged
973
- else
974
- # Multi-row mode: find max row count across db_shards
975
- max_rows = 0
976
- db_shard_groups.each do |db_shard, _group|
977
- result = db_shard_results[db_shard]
978
- next if result == {}
979
- count = result.is_a?(Array) ? result.length : 0
980
- max_rows = count if count > max_rows
981
- end
982
- return {} if max_rows == 0
983
-
984
- merged = Array.new(max_rows) { Array.new(items.length) { [nil, nil] } }
985
- db_shard_groups.each do |db_shard, group|
986
- result = db_shard_results[db_shard]
987
- next if result == {}
988
- rows = result.is_a?(Array) ? result : []
989
- rows.each_with_index do |row, row_num|
990
- next unless row.is_a?(Array)
991
- group[:positions].each_with_index do |orig_pos, db_shard_idx|
992
- merged[row_num][orig_pos] = row[db_shard_idx] if row[db_shard_idx]
993
- end
994
- end
995
- end
996
- merged
997
971
  end
972
+ # Match the single db_shard behavior of returning a flat array for a single row
973
+ return merged[0] if max_rows == 1
974
+ merged
998
975
  end
999
976
 
1000
977
  # Execute a tsdb_lookup query against a single db_shard.
1001
978
  # This contains the original ASOF JOIN logic for items all on the same QuestDB instance.
1002
- def self.tsdb_lookup_single_db_shard(items, start_time:, end_time: nil, scope: "DEFAULT", db_shard: 0)
979
+ # When flatten is true a single row result is returned as a flat array rather than an array of rows.
980
+ def self.tsdb_lookup_single_db_shard(items, start_time:, end_time: nil, scope: "DEFAULT", db_shard: 0, flatten: true)
1003
981
  tables = {}
1004
982
  names = []
1005
983
  nil_count = 0
@@ -1060,6 +1038,11 @@ module OpenC3
1060
1038
  end
1061
1039
  end
1062
1040
 
1041
+ # Every item in this db_shard is a placeholder so there's no table to query.
1042
+ # Return no results and let tsdb_lookup fill these positions with [nil, nil]
1043
+ # when it merges the db_shards (an all placeholder lookup returns before this)
1044
+ return {} if tables.empty?
1045
+
1063
1046
  # Add needed timestamp columns to the SELECT for calculated items
1064
1047
  needed_timestamps.each do |table_index, ts_columns|
1065
1048
  ts_columns.each do |ts_col|
@@ -1147,7 +1130,7 @@ module OpenC3
1147
1130
  data[row_num].insert(position, [calculated_value, nil])
1148
1131
  end
1149
1132
  end
1150
- if result.ntuples == 1
1133
+ if flatten and result.ntuples == 1
1151
1134
  data = data[0]
1152
1135
  end
1153
1136
  data
File without changes
File without changes
File without changes
@@ -0,0 +1,39 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2026 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is distributed in the hope that it will be useful,
7
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ # See LICENSE.md for more details.
10
+ #
11
+ # This file may also be used under the terms of a commercial license
12
+ # if purchased from OpenC3, Inc.
13
+
14
+ require 'uri'
15
+ require 'openc3/utilities/logger'
16
+
17
+ module OpenC3
18
+ class RubygemsUrl
19
+ DEFAULT = 'https://rubygems.org'
20
+
21
+ # Validate that a resolved rubygems_url is an http(s) URL before it is used
22
+ # as a Gem source. The value can come from a user-writable setting or ENV, so
23
+ # a malformed or non-http value is rejected and replaced with the default
24
+ # rather than passed through to RubyGems.
25
+ #
26
+ # @param rubygems_url [String] the resolved rubygems source url to validate
27
+ # @return [String] the original url if valid, otherwise DEFAULT
28
+ def self.validate(rubygems_url)
29
+ uri = URI.parse(rubygems_url)
30
+ unless uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
31
+ raise URI::InvalidURIError, "not an http(s) URL"
32
+ end
33
+ rubygems_url
34
+ rescue URI::InvalidURIError => e
35
+ Logger.error("Invalid rubygems_url '#{rubygems_url}' (#{e.message}); falling back to #{DEFAULT}")
36
+ DEFAULT
37
+ end
38
+ end
39
+ end
@@ -28,6 +28,7 @@ require 'openc3/utilities/script'
28
28
  require 'openc3/utilities/store'
29
29
  require 'openc3/utilities/store_queued'
30
30
  require 'openc3/utilities/bucket_require'
31
+ require 'openc3/utilities/python_venv'
31
32
  require 'openc3/models/offline_access_model'
32
33
  require 'openc3/models/environment_model'
33
34
  require 'openc3/models/script_engine_model'
@@ -36,7 +37,7 @@ require 'fileutils'
36
37
 
37
38
  if not defined? RAILS_ROOT
38
39
  if ENV['RAILS_ROOT']
39
- RAILS_ROOT = ENV['RAILS_ROOT']
40
+ RAILS_ROOT = ENV.fetch('RAILS_ROOT')
40
41
  elsif defined? Rails
41
42
  RAILS_ROOT = Rails.root
42
43
  else
@@ -436,7 +437,7 @@ class RunningScript
436
437
  extension = File.extname(name).to_s.downcase
437
438
  script_engine = nil
438
439
  if extension == '.py'
439
- process_name = ENV['OPENC3_PYTHON_BIN'] || '/openc3/python/.venv/bin/python'
440
+ process_name = ENV.fetch('OPENC3_PYTHON_BIN', '/openc3/python/.venv/bin/python')
440
441
  runner_path = File.join(RAILS_ROOT, 'scripts', 'run_script.py')
441
442
  elsif extension == '.rb'
442
443
  process_name = 'ruby'
@@ -449,7 +450,7 @@ class RunningScript
449
450
  if script_engine_model
450
451
  script_engine = script_engine_model.filename
451
452
  if File.extname(script_engine).to_s.downcase == '.py'
452
- process_name = ENV['OPENC3_PYTHON_BIN'] || '/openc3/python/.venv/bin/python'
453
+ process_name = ENV.fetch('OPENC3_PYTHON_BIN', '/openc3/python/.venv/bin/python')
453
454
  runner_path = File.join(RAILS_ROOT, 'scripts', 'run_script.py')
454
455
  else
455
456
  process_name = 'ruby'
@@ -521,17 +522,17 @@ class RunningScript
521
522
 
522
523
  # Set proper secrets for running script
523
524
  process.environment['SECRET_KEY_BASE'] = nil
524
- process.environment['OPENC3_REDIS_USERNAME'] = ENV['OPENC3_SR_REDIS_USERNAME']
525
- process.environment['OPENC3_REDIS_PASSWORD'] = ENV['OPENC3_SR_REDIS_PASSWORD']
526
- process.environment['OPENC3_BUCKET_USERNAME'] = ENV['OPENC3_SR_BUCKET_USERNAME']
527
- process.environment['OPENC3_BUCKET_PASSWORD'] = ENV['OPENC3_SR_BUCKET_PASSWORD']
525
+ process.environment['OPENC3_REDIS_USERNAME'] = ENV.fetch('OPENC3_SR_REDIS_USERNAME', nil)
526
+ process.environment['OPENC3_REDIS_PASSWORD'] = ENV.fetch('OPENC3_SR_REDIS_PASSWORD', nil)
527
+ process.environment['OPENC3_BUCKET_USERNAME'] = ENV.fetch('OPENC3_SR_BUCKET_USERNAME', nil)
528
+ process.environment['OPENC3_BUCKET_PASSWORD'] = ENV.fetch('OPENC3_SR_BUCKET_PASSWORD', nil)
528
529
  process.environment['OPENC3_SR_REDIS_USERNAME'] = nil
529
530
  process.environment['OPENC3_SR_REDIS_PASSWORD'] = nil
530
531
  process.environment['OPENC3_SR_BUCKET_USERNAME'] = nil
531
532
  process.environment['OPENC3_SR_BUCKET_PASSWORD'] = nil
532
- process.environment['OPENC3_API_CLIENT'] = ENV['OPENC3_API_CLIENT']
533
+ process.environment['OPENC3_API_CLIENT'] = ENV.fetch('OPENC3_API_CLIENT', nil)
533
534
  if model and model.offline_access_token
534
- auth = OpenC3::OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
535
+ auth = OpenC3::OpenC3KeycloakAuthentication.new(ENV.fetch('OPENC3_KEYCLOAK_URL', nil))
535
536
  valid_token = auth.get_token_from_refresh_token(model.offline_access_token)
536
537
  if valid_token
537
538
  process.environment['OPENC3_API_TOKEN'] = model.offline_access_token
@@ -541,58 +542,26 @@ class RunningScript
541
542
  raise "offline_access token invalid for script"
542
543
  end
543
544
  else
544
- process.environment['OPENC3_API_USER'] = ENV['OPENC3_API_USER']
545
+ process.environment['OPENC3_API_USER'] = ENV.fetch('OPENC3_API_USER', nil)
545
546
  if ENV['OPENC3_SERVICE_PASSWORD']
546
- process.environment['OPENC3_API_PASSWORD'] = ENV['OPENC3_SERVICE_PASSWORD']
547
+ process.environment['OPENC3_API_PASSWORD'] = ENV.fetch('OPENC3_SERVICE_PASSWORD')
547
548
  else
548
549
  raise "No authentication available for script"
549
550
  end
550
551
  end
551
- process.environment['GEM_HOME'] = ENV['GEM_HOME']
552
+ process.environment['GEM_HOME'] = ENV.fetch('GEM_HOME', nil)
552
553
 
553
- # Resolve the per-plugin Python venv for this script. For saved scripts
554
- # we look up which plugin owns the target and check whether that plugin
555
- # has an isolated UV venv. For temp scripts the frontend can pass a
556
- # python_venv name directly, skipping the target lookup entirely.
557
- python_venv_dir = nil
558
- begin
559
- target_name = name.split('/')[0].to_s.upcase
560
- if target_name == '__TEMP__' && python_venv
561
- # File.basename prevents path traversal (strips directory components)
562
- safe_name = File.basename(python_venv.to_s)
563
- candidate = "/gems/plugin_venvs/#{safe_name}/.venv"
564
- python_venv_dir = candidate if File.directory?(candidate)
565
- else
566
- target_info = OpenC3::TargetModel.get(name: target_name, scope: scope)
567
- if target_info && target_info['plugin']
568
- sanitized_name = "#{scope}__#{target_info['plugin']}".tr('^a-zA-Z0-9_-', '_')
569
- candidate = "/gems/plugin_venvs/#{sanitized_name}/.venv"
570
- python_venv_dir = candidate if File.directory?(candidate)
571
- end
572
- end
573
- rescue => e
574
- OpenC3::Logger.debug("Could not resolve plugin venv for script '#{name}': #{e.message}")
575
- end
576
-
577
- if python_venv_dir
578
- process.environment['VIRTUAL_ENV'] = python_venv_dir
579
- process.environment['PATH'] = "#{python_venv_dir}/bin:#{ENV.fetch('PATH', '')}"
580
- process.environment['PYTHONUSERBASE'] = python_venv_dir
581
- # Add plugin venv site-packages to PYTHONPATH so the base venv's Python
582
- # binary can find plugin-specific packages. PYTHONPATH is always respected
583
- # by CPython regardless of venv activation state.
584
- site_packages = Dir.glob("#{python_venv_dir}/lib/python*/site-packages").first
585
- existing_pythonpath = ENV.fetch('PYTHONPATH', '')
586
- if site_packages
587
- process.environment['PYTHONPATH'] = existing_pythonpath.empty? ? site_packages : "#{site_packages}:#{existing_pythonpath}"
588
- else
589
- process.environment['PYTHONPATH'] = existing_pythonpath.empty? ? nil : existing_pythonpath
590
- end
591
- else
592
- system_venv = File.dirname(ENV['OPENC3_PYTHON_BIN'] || '/openc3/python/.venv/bin/python').chomp('/bin')
554
+ python_venv_dir = OpenC3::PythonVenv.configure_for_script(
555
+ process.environment, name: name, scope: scope, python_venv: python_venv
556
+ )
557
+ unless python_venv_dir
558
+ system_venv = File.dirname(ENV.fetch('OPENC3_PYTHON_BIN', '/openc3/python/.venv/bin/python')).chomp('/bin')
593
559
  process.environment['VIRTUAL_ENV'] = system_venv
594
- process.environment['PYTHONUSERBASE'] = ENV['PYTHONUSERBASE']
595
- process.environment['PYTHONPATH'] = ENV['PYTHONPATH']
560
+ process.environment['PYTHONUSERBASE'] = ENV.fetch('PYTHONUSERBASE', OpenC3::PythonVenv::DEFAULT_PYTHONUSERBASE)
561
+ # Deliberately nil when PYTHONPATH is unset rather than defaulting to
562
+ # something like '.', which would put the script's own working directory
563
+ # on the import path and let a local file shadow a real module.
564
+ process.environment['PYTHONPATH'] = ENV.fetch('PYTHONPATH', nil)
596
565
  end
597
566
 
598
567
  # Spawned process should not be controlled by same Bundler constraints as spawning process
@@ -1556,7 +1525,7 @@ class RunningScript
1556
1525
  $stdout = OpenC3::Stdout.instance
1557
1526
  $stderr = OpenC3::Stderr.instance
1558
1527
  OpenC3::Logger.stdout = true
1559
- OpenC3::Logger.level = OpenC3::Logger::INFO
1528
+ OpenC3::Logger.level = OpenC3::Logger.default_level
1560
1529
  end
1561
1530
 
1562
1531
  def output_thread
@@ -3,9 +3,9 @@ require 'aws-sdk-s3'
3
3
  if ENV['OPENC3_CLOUD'] == 'local'
4
4
  Aws.config.update(
5
5
  s3: {
6
- endpoint: ENV['OPENC3_BUCKET_URL'] || (ENV['OPENC3_DEVEL'] ? 'http://127.0.0.1:9000' : 'http://openc3-buckets:9000'),
7
- access_key_id: ENV['OPENC3_BUCKET_USERNAME'],
8
- secret_access_key: ENV['OPENC3_BUCKET_PASSWORD'],
6
+ endpoint: ENV.fetch('OPENC3_BUCKET_URL', nil) || (ENV['OPENC3_DEVEL'] ? 'http://127.0.0.1:9000' : 'http://openc3-buckets:9000'),
7
+ access_key_id: ENV.fetch('OPENC3_BUCKET_USERNAME', nil),
8
+ secret_access_key: ENV.fetch('OPENC3_BUCKET_PASSWORD', nil),
9
9
  force_path_style: true,
10
10
  region: 'us-east-1',
11
11
  # Disable automatic checksum calculation for S3-compatible backends (versitygw)
@@ -20,9 +20,9 @@ if ENV['OPENC3_CLOUD'] == 'local'
20
20
  else # AWS
21
21
  Aws.config.update(
22
22
  s3: {
23
- endpoint: "https://s3.#{ENV['AWS_REGION']}.amazonaws.com",
23
+ endpoint: "https://s3.#{ENV.fetch('AWS_REGION', nil)}.amazonaws.com",
24
24
  force_path_style: true,
25
- region: ENV['AWS_REGION']
25
+ region: ENV.fetch('AWS_REGION', nil)
26
26
  }
27
27
  )
28
28
  end
@@ -17,6 +17,7 @@
17
17
 
18
18
  require 'tempfile'
19
19
  require 'openc3/utilities/target_file'
20
+ require 'openc3/utilities/python_venv'
20
21
  require 'openc3/utilities/running_script'
21
22
  require 'openc3/script/suite'
22
23
  require 'openc3/script/suite_runner'
@@ -37,12 +38,6 @@ class Script < OpenC3::TargetFile
37
38
  super(scope, nil, target: target) # No path matchers
38
39
  end
39
40
 
40
- # Split off the '*' that indicates a file is modified on the server
41
- def self.strip_modified(name)
42
- name.split('*')[0]
43
- end
44
- private_class_method :strip_modified
45
-
46
41
  def self.lock(scope, name, username)
47
42
  OpenC3::Store.hset("#{scope}__script-locks", strip_modified(name), username)
48
43
  end
@@ -88,7 +83,7 @@ class Script < OpenC3::TargetFile
88
83
  username: username, comment: comment)
89
84
  end
90
85
 
91
- def self.process_suite(name, contents, new_process: true, username: nil, scope:)
86
+ def self.process_suite(name, contents, new_process: true, username: nil, scope:, python_venv: nil)
92
87
  python = false
93
88
  python = true if File.extname(name) == '.py'
94
89
 
@@ -155,10 +150,19 @@ class Script < OpenC3::TargetFile
155
150
  end
156
151
  end
157
152
  process.environment['GEM_HOME'] = ENV['GEM_HOME'] || '/gems'
158
- process.environment['PYTHONUSERBASE'] = ENV['PYTHONUSERBASE'] || '/gems/python_packages'
153
+ process.environment['PYTHONUSERBASE'] = ENV['PYTHONUSERBASE'] || OpenC3::PythonVenv::DEFAULT_PYTHONUSERBASE
159
154
  # Preserve PYTHONPATH to ensure Python can find both UV venv and user packages
160
155
  process.environment['PYTHONPATH'] = ENV['PYTHONPATH'] || '.'
161
156
 
157
+ # Suite analysis executes Python in a separate process before the script
158
+ # itself is started. Give that process the same plugin venv visibility as
159
+ # RunningScript so imports used while defining a suite can be resolved.
160
+ if python
161
+ OpenC3::PythonVenv.configure_for_script(
162
+ process.environment, name: name, scope: scope, python_venv: python_venv
163
+ )
164
+ end
165
+
162
166
  # Spawned process should not be controlled by same Bundler constraints as spawning process
163
167
  ENV.each do |key, _value|
164
168
  if key =~ /^BUNDLE/
@@ -15,13 +15,58 @@ ENV['OPENC3_SECRET_BACKEND'] ||= 'redis'
15
15
 
16
16
  module OpenC3
17
17
  class Secrets
18
+ # Default base directory that FILE type secret paths must reside under.
19
+ # FILE type secrets are written by the operator (or mounted by Kubernetes)
20
+ # and then read back by the microservice, so the path is a destination we
21
+ # control, not an arbitrary file on the host.
22
+ DEFAULT_SECRET_FILE_DIR = '/tmp'
23
+
18
24
  def initialize
19
25
  @local_secrets = {}
20
26
  end
21
27
 
28
+ # Base directory that FILE type secret paths must reside under
29
+ def self.secret_file_dir
30
+ dir = ENV.fetch('OPENC3_SECRET_FILE_DIR', nil)
31
+ dir = DEFAULT_SECRET_FILE_DIR if dir.nil? or dir.empty?
32
+ dir
33
+ end
34
+
35
+ # Validates a FILE type secret path. The path must expand to a location
36
+ # strictly inside Secrets.secret_file_dir. This prevents a plugin
37
+ # configuration such as 'SECRET FILE KEY /root/.ssh/id_rsa' or
38
+ # 'SECRET FILE KEY /tmp/../etc/shadow' from reading or overwriting arbitrary
39
+ # files as the OpenC3 process user.
40
+ #
41
+ # @param path [String] Path from a SECRET FILE definition
42
+ # @return [String] The validated absolute path
43
+ def self.validate_file_path(path)
44
+ raise ArgumentError, "Secret file path must be a String but is a #{path.class}" unless path.is_a?(String)
45
+ raise ArgumentError, "Secret file path must not be blank" if path.strip.empty?
46
+ raise ArgumentError, "Secret file path must not contain a null byte" if path.include?("\x00")
47
+
48
+ # expand_path collapses '..' and '.', expands '~', and makes a relative path
49
+ # absolute, so the comparison below can't be walked out of. This is
50
+ # deliberately a purely lexical check: it does not touch the filesystem, so
51
+ # a path validates identically at plugin install time (cmd-tlm-api) and at
52
+ # write time (openc3-operator), which are separate containers. Symlinks are
53
+ # rejected by the operator when it writes the secret.
54
+ base = File.expand_path(secret_file_dir)
55
+ absolute = File.expand_path(path)
56
+ unless absolute.start_with?(base + File::SEPARATOR)
57
+ raise ArgumentError, "Secret file path '#{path}' must be under '#{base}'"
58
+ end
59
+ absolute
60
+ end
61
+
62
+ # @return true if path is inside base, or is base itself
63
+ def self.path_contained?(base, path)
64
+ path == base or path.start_with?(base.end_with?(File::SEPARATOR) ? base : base + File::SEPARATOR)
65
+ end
66
+
22
67
  def self.getClient
23
68
  raise 'OPENC3_SECRET_BACKEND environment variable is required' unless ENV['OPENC3_SECRET_BACKEND']
24
- secrets_class = ENV['OPENC3_SECRET_BACKEND'].capitalize + 'Secrets'
69
+ secrets_class = ENV.fetch('OPENC3_SECRET_BACKEND').capitalize + 'Secrets'
25
70
  klass = OpenC3.require_class('openc3/utilities/' + secrets_class.class_name_to_filename)
26
71
  klass.new
27
72
  end
@@ -50,9 +95,9 @@ module OpenC3
50
95
  type, key, data, _secret_store = secret
51
96
  case type
52
97
  when 'ENV'
53
- @local_secrets[key] = ENV[data]
98
+ @local_secrets[key] = ENV.fetch(data, nil)
54
99
  when 'FILE'
55
- @local_secrets[key] = File.read(data)
100
+ @local_secrets[key] = File.read(Secrets.validate_file_path(data))
56
101
  else
57
102
  raise "Unknown secret type: #{type}"
58
103
  end
File without changes
File without changes
File without changes
@@ -116,8 +116,8 @@ module OpenC3
116
116
  end
117
117
 
118
118
  def initialize(pool_size = 10, db_shard: 0)
119
- @redis_username = ENV['OPENC3_REDIS_USERNAME']
120
- @redis_key = ENV['OPENC3_REDIS_PASSWORD']
119
+ @redis_username = ENV.fetch('OPENC3_REDIS_USERNAME', nil)
120
+ @redis_key = ENV.fetch('OPENC3_REDIS_PASSWORD', nil)
121
121
  hostname = ENV['OPENC3_REDIS_HOSTNAME'].to_s.gsub("SHARDNUM", db_shard.to_s)
122
122
  @redis_url = "redis://#{hostname}:#{ENV.fetch('OPENC3_REDIS_PORT', 6379)}"
123
123
  @redis_pool = StoreConnectionPool.new(size: pool_size) { build_redis() }
File without changes