openc3 7.3.0 → 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.
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 +138 -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 +4 -3
  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 +4 -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 +0 -0
  172. data/lib/openc3/io/json_drb.rb +20 -4
  173. data/lib/openc3/io/json_drb_object.rb +0 -0
  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 +0 -0
  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 +546 -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 +0 -0
  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 +0 -0
  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 +0 -0
  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 +0 -0
  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 +5 -5
  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 +36 -9
@@ -15,27 +15,570 @@
15
15
  # This file may also be used under the terms of a commercial license
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
+ require 'json'
19
+ require 'openc3/config/config_parser'
18
20
  require 'openc3/models/model'
19
21
 
20
22
  module OpenC3
21
23
  class SettingModel < Model
22
24
  PRIMARY_KEY = 'openc3__settings'
23
25
 
26
+ # One environment variable per setting: OPENC3_SETTING_TIME_ZONE=UTC sets
27
+ # the 'time_zone' setting. A prefix scan rather than an enumerated list, so
28
+ # a setting added by a later release needs no change here. Deliberately not
29
+ # a single JSON blob variable: embedding JSON in a compose environment
30
+ # list, a Helm value and an ECS task definition each need different
31
+ # escaping, and one variable per setting reads the same in all three.
32
+ SETTING_ENV_PREFIX = 'OPENC3_SETTING_'
33
+
34
+ # Set to true to write the values on every init rather than only when the
35
+ # setting is missing. Use when the environment is the source of truth and
36
+ # Admin Console edits should not survive a restart.
37
+ #
38
+ # Unlike the OPENC3_NO_* install flags, this is NOT enabled by presence:
39
+ # it discards whatever an operator configured in the Admin Console, so
40
+ # OPENC3_SETTINGS_OVERWRITE=false and =0 mean off, as they read. An
41
+ # unrecognized value is an error rather than a silent guess.
42
+ #
43
+ # Note this does not start with SETTING_ENV_PREFIX, so the prefix scan
44
+ # can't mistake it for a setting named 'overwrite'.
45
+ OVERWRITE_ENV_VAR = 'OPENC3_SETTINGS_OVERWRITE'
46
+
47
+ # Set to true to allow setting names that are not in KNOWN_SETTINGS.
48
+ # Needed for a setting added by a newer tool than this library knows about;
49
+ # without it a misspelled name is reported and skipped.
50
+ ALLOW_UNKNOWN_ENV_VAR = 'OPENC3_SETTINGS_ALLOW_UNKNOWN'
51
+
52
+ # Set to true to make a rejected setting fail init instead of being
53
+ # reported and skipped. Off by default: the init container restarts on
54
+ # failure, so a typo in a cosmetic setting would otherwise crash loop COSMOS
55
+ # with the cause buried in restarting container logs, and the operator ends
56
+ # up with no COSMOS rather than COSMOS with one default time zone. Turn it on
57
+ # for a deployment that would rather not come up than come up misconfigured.
58
+ #
59
+ # `initsettings --dry-run` fails regardless - a preflight check exists to.
60
+ STRICT_ENV_VAR = 'OPENC3_SETTINGS_STRICT'
61
+
62
+
63
+ # Every setting that can be seeded from the environment.
64
+ #
65
+ # An unknown name is reported and skipped rather than written: nothing reads
66
+ # it, so the result of a typo is a dead Redis key plus a setting the
67
+ # operator believes they configured and did not. Init still continues -
68
+ # ALLOW_UNKNOWN_ENV_VAR opts out of the check entirely.
69
+ #
70
+ # TO ADD A SETTING, add a row here. The name is the string the Admin
71
+ # Console component passes to loadSetting/saveSetting - find it in
72
+ # openc3-cosmos-init/plugins/packages/openc3-vue-common/src/tools/admin/
73
+ # tabs/settings/<Name>Settings.vue, e.g. TimeZoneSettings.vue has
74
+ # `const settingName = 'time_zone'`. Then:
75
+ #
76
+ # type: what the reader SAVES and expects back, not what a control
77
+ # displays. Storing the wrong shape is silent - the reader either
78
+ # throws or ignores the value:
79
+ # :string plain text (a URL, a subtitle)
80
+ # :boolean real true/false; the frontend treats the string
81
+ # "false" as truthy, so these must not be text
82
+ # :json_text JSON kept as a String. A component that calls
83
+ # JSON.stringify before saveSetting and JSON.parse
84
+ # in parseSetting is this - handing it a parsed
85
+ # object makes its own JSON.parse throw
86
+ # :json JSON parsed into an object before storing, for a
87
+ # reader that checks the shape. AiChatConfig.load
88
+ # does `raw['data'].is_a?(Hash) ? ... : {}`, so text
89
+ # would be silently discarded
90
+ # values: the allowed values, or nil for free text.
91
+ # Copy them from the component's v-select items.
92
+ # require_keys: for :json/:json_text, top-level keys the blob must have.
93
+ # Use when a partial blob would break the reader rather than just
94
+ # fall back to a default. See 'system_health' below.
95
+ # example: for :json/:json_text, a valid value. There is no other way for
96
+ # an operator to discover the shape of a blob short of reading the
97
+ # component source, so every JSON setting needs one. Copy it from
98
+ # the JSON.stringify({...}) in the component's save method.
99
+ #
100
+ # Example, for a hypothetical LogLevelSettings.vue holding 'log_level':
101
+ #
102
+ # 'log_level' => { type: :string, values: ['DEBUG', 'INFO', 'WARN'] },
103
+ #
104
+ # No other change is needed - the env var (OPENC3_SETTING_LOG_LEVEL), the
105
+ # validation and the `cli initsettings --help` listing all follow.
106
+ KNOWN_SETTINGS = {
107
+ # Booleans. The frontend treats the string "false" as truthy, so these
108
+ # have to reach Redis as real booleans.
109
+ 'ai_chat' => { type: :boolean, values: nil },
110
+ 'news_feed' => { type: :boolean, values: nil },
111
+ 'script_runner_locking' => { type: :boolean, values: nil },
112
+ 'script_runner_lifecycle' => { type: :boolean, values: nil }, # Enterprise only
113
+ # Fixed choice strings
114
+ 'time_zone' => { type: :string, values: ['local', 'UTC'] },
115
+ 'time_format' => { type: :string, values: ['ampm', '24hr'] },
116
+ 'theme' => { type: :string, values: ['cosmosDark', 'cosmosDarkCobalt', 'cosmosDarkIndigo',
117
+ 'cosmosDarkSlate', 'cosmosDarkEmerald'] },
118
+ # Free text
119
+ 'subtitle' => { type: :string, values: nil },
120
+ 'source_url' => { type: :string, values: nil },
121
+ 'rubygems_url' => { type: :string, values: nil },
122
+ 'pypi_url' => { type: :string, values: nil },
123
+ # JSON *text*: these components JSON.stringify before saving and
124
+ # JSON.parse on load, so the stored value is a String, not an object
125
+ 'astro' => { type: :json_text, values: nil,
126
+ example: '{"hideClock":false}' },
127
+ # require_keys because the Admin Console writes all five together and both
128
+ # readers assume that: a missing height reaches the stylesheet as
129
+ # "height: undefinedpx", which is invalid CSS, so the banner renders at
130
+ # auto height top AND bottom instead of staying hidden.
131
+ 'classification_banner' => { type: :json_text, values: nil,
132
+ require_keys: ['text', 'fontColor', 'backgroundColor',
133
+ 'topHeight', 'bottomHeight'],
134
+ example: '{"text":"UNCLASSIFIED","fontColor":"#ffffff",' \
135
+ '"backgroundColor":"#00cc00","topHeight":20,"bottomHeight":0}' },
136
+ 'context_tag' => { type: :json_text, values: nil,
137
+ example: '{"text":"DEV","fontColor":"#ffffff","backgroundColor":"#ff0000"}' },
138
+ # Settings with no Admin Console tab of their own - see NO_ADMIN_TAB below
139
+ #
140
+ # Written as JSON text by ScopeModel#seed_database and re-read by the
141
+ # Enterprise metrics microservices. require_keys because a partial blob
142
+ # doesn't degrade gracefully: log_thresholds does
143
+ # data['global']['enableAlerts'] and passes data[metric_name] straight to
144
+ # check_persistent_threshold, so a blob missing either key raises rather
145
+ # than falling back, silently ending CPU/memory/disk alerting.
146
+ 'system_health' => { type: :json_text, values: nil,
147
+ require_keys: ['cpu', 'memory', 'disk', 'global'],
148
+ example: '{"cpu":{"redThreshold":90.0,"yellowThreshold":80.0,"snoozeMinutes":15,' \
149
+ '"sustainedSeconds":15},"memory":{"redThreshold":90.0,' \
150
+ '"yellowThreshold":80.0,"snoozeMinutes":15,"sustainedSeconds":15},' \
151
+ '"disk":{"redThreshold":90.0,"yellowThreshold":80.0,' \
152
+ '"snoozeMinutes":720,"sustainedSeconds":60},' \
153
+ '"global":{"enableAlerts":true}}' },
154
+ # Enterprise AI chat provider/model config. :json, not :json_text -
155
+ # AiChatConfig.load ignores a String and falls back to {}
156
+ 'ai_chat_config' => { type: :json, values: nil,
157
+ example: '{"provider":"anthropic","model":"claude-opus-4-7","base_url":""}' },
158
+ }
159
+
160
+ # Settings that KNOWN_SETTINGS lists on purpose despite having no
161
+ # *Settings.vue tab in this repo. The drift specs compare KNOWN_SETTINGS
162
+ # against those components, so without this list adding either of these
163
+ # would fail the "doesn't list a setting the Admin Console no longer has"
164
+ # check. Both are real settings that code reads.
165
+ NO_ADMIN_TAB = ['system_health', 'ai_chat_config']
166
+
24
167
  # NOTE: The following three class methods are used by the ModelController
25
168
  # and are reimplemented to enable various Model class methods to work
26
- def self.get(name:, scope: nil)
169
+ def self.get(name:, scope: nil) # NOSONAR - scope: is part of the caller-facing signature
27
170
  super(PRIMARY_KEY, name: name)
28
171
  end
29
172
 
30
- def self.names(scope: nil)
173
+ def self.names(scope: nil) # NOSONAR - scope: is part of the caller-facing signature
31
174
  super(PRIMARY_KEY)
32
175
  end
33
176
 
34
- def self.all(scope: nil)
177
+ def self.all(scope: nil) # NOSONAR - scope: is part of the caller-facing signature
35
178
  super(PRIMARY_KEY)
36
179
  end
37
180
  # END NOTE
38
181
 
182
+ # Seed settings from OPENC3_SETTING_<NAME> environment variables. Called by
183
+ # `openc3cli initsettings` during init container startup.
184
+ #
185
+ # By default a setting is only written when it does not already exist, so a
186
+ # value changed in the Admin Console survives a container restart. Set
187
+ # OPENC3_SETTINGS_OVERWRITE to write on every run instead.
188
+ #
189
+ # Nothing here aborts init by default. A bad setting name, a bad value, or a
190
+ # malformed control variable is reported on stdout and that one setting is
191
+ # skipped, leaving COSMOS on its built-in default. Set STRICT_ENV_VAR to
192
+ # fail init instead; --dry-run always fails.
193
+ #
194
+ # @param env [Hash] environment to read from, defaults to ENV
195
+ # @param overwrite [Boolean, nil] nil reads OVERWRITE_ENV_VAR from env
196
+ # @param strict [Boolean, nil] nil reads STRICT_ENV_VAR from env
197
+ # @param dry_run [Boolean] report what would happen and write nothing.
198
+ # Reports every problem rather than aborting on the first, and works
199
+ # before Redis is up so it can be run ahead of starting COSMOS.
200
+ # @return [Array<String>] names of the settings that were written
201
+ def self.apply_defaults(env: ENV, overwrite: nil, dry_run: false, strict: nil)
202
+ problems = []
203
+ strict = read_control_flag(env, STRICT_ENV_VAR, problems) if strict.nil?
204
+ overwrite = read_control_flag(env, OVERWRITE_ENV_VAR, problems) if overwrite.nil?
205
+ allow_unknown = read_control_flag(env, ALLOW_UNKNOWN_ENV_VAR, problems)
206
+ # Collects a value that can't be coerced, so OPENC3_SETTING_AI_CHAT=nope is
207
+ # reported like any other bad value rather than escaping as an exception
208
+ settings = parse_defaults_env(env, problems)
209
+
210
+ prefix = dry_run ? '[dry run] ' : ''
211
+ written = []
212
+ if settings.empty?
213
+ # Only when nothing matched the prefix. If every matching variable failed
214
+ # to coerce, settings is empty too, and claiming none were set is a lie -
215
+ # the errors reported below are the explanation
216
+ if problems.empty?
217
+ puts "#{prefix}No #{SETTING_ENV_PREFIX}* environment variables set - nothing to seed"
218
+ end
219
+ else
220
+ # A dry run is most useful before `openc3.sh start`, when there is no
221
+ # Redis to compare against. Names and values can still be checked.
222
+ comparable = dry_run ? redis_available? : true
223
+ puts "#{prefix}Redis is not reachable - checking names and values only" unless comparable
224
+
225
+ settings.each do |name, value|
226
+ begin
227
+ validate_setting!(name, value, allow_unknown: allow_unknown)
228
+ rescue StandardError => error
229
+ # Collect rather than abort. The init container restarts on failure
230
+ # (compose restart: on-failure, Kubernetes restartPolicy OnFailure),
231
+ # so raising here puts COSMOS in a crash loop over a cosmetic
232
+ # setting, with the cause buried in restarting container logs.
233
+ # Skipping leaves the setting at its default, which is the same
234
+ # outcome as not setting it, and the error is reported below.
235
+ problems << error.message
236
+ next
237
+ end
238
+
239
+ existing = comparable ? get(name: name) : nil
240
+ action, message = plan_setting(name, value, existing, overwrite)
241
+ message += ' (current value unknown)' unless comparable
242
+ puts "#{prefix}#{message}"
243
+ next if action == :skip
244
+
245
+ # :record leaves the setting alone and only refreshes provenance, so it
246
+ # is not reported as written
247
+ written << name if action == :write
248
+ next if dry_run
249
+ set({ name: name, data: value }, scope: nil) if action == :write
250
+ record_seeded(name, value)
251
+ end
252
+ end
253
+
254
+ report_problems(problems, prefix: prefix, dry_run: dry_run, strict: strict)
255
+ written
256
+ end
257
+
258
+ # Print every problem and decide whether it should end the process.
259
+ #
260
+ # A typo in a cosmetic setting must not put the init container in a restart
261
+ # loop with the cause buried in restarting container logs, so the default is
262
+ # report-and-continue. Two things opt into failing: --dry-run, which exists
263
+ # to be a preflight gate, and STRICT_ENV_VAR, for a deployment that would
264
+ # rather not come up at all than come up misconfigured.
265
+ def self.report_problems(problems, prefix:, dry_run:, strict:)
266
+ return if problems.empty?
267
+ problems.each { |problem| puts "#{prefix}ERROR: #{problem}" }
268
+ # "problem" rather than "setting" - a malformed control variable is
269
+ # reported here too, and it isn't a setting that got skipped
270
+ summary = "#{problems.length} #{SETTING_ENV_PREFIX}* configuration problem(s)"
271
+ if dry_run or strict
272
+ puts "#{prefix}#{summary}"
273
+ else
274
+ puts "#{prefix}#{summary} - the affected setting(s) were skipped and COSMOS will use the default"
275
+ puts "#{prefix}Set #{STRICT_ENV_VAR} to fail init on these instead of continuing"
276
+ end
277
+ $stdout.flush
278
+ raise "#{summary}: #{problems.join('; ')}" if dry_run or strict
279
+ end
280
+
281
+ # Read a boolean control variable, reporting an unparsable value rather than
282
+ # letting it abort init. Off is the safe reading of all three: OVERWRITE off
283
+ # doesn't discard an Admin Console edit, ALLOW_UNKNOWN off doesn't write a
284
+ # name nothing reads, and STRICT off doesn't fail init.
285
+ #
286
+ # @param problems [Array<String>] collects the message when the value is bad
287
+ # @return [Boolean]
288
+ def self.read_control_flag(env, name, problems)
289
+ truthy_env?(env, name)
290
+ rescue StandardError => error
291
+ problems << "#{error.message} - treating #{name} as off"
292
+ false
293
+ end
294
+
295
+ # What apply_defaults will do with one setting. Single-sourced so a dry run
296
+ # cannot report one thing and the real run do another.
297
+ #
298
+ # @return [Array(Symbol, String)] :write, :record or :skip, and the line to
299
+ # log. :record means the stored value is already correct but provenance
300
+ # still needs writing, so the setting itself is left untouched.
301
+ def self.plan_setting(name, value, existing, overwrite)
302
+ if existing.nil?
303
+ [:write, "Set default setting '#{name}' to: #{value.inspect}"]
304
+ elsif overwrite
305
+ if existing['data'] == value
306
+ # Rewriting the same value would bump updated_at on every init and
307
+ # report a write that changed nothing. Provenance is still recorded,
308
+ # so the next run without OVERWRITE can tell this value came from the
309
+ # environment rather than from an Admin Console edit.
310
+ [:record, "Setting '#{name}' already matches #{value.inspect}"]
311
+ else
312
+ # Overwrite discards an Admin Console edit, so say what was lost -
313
+ # otherwise the log reads identically to a first-time seed
314
+ [:write, "Overwriting setting '#{name}': #{existing['data'].inspect} -> " \
315
+ "#{value.inspect} (#{OVERWRITE_ENV_VAR} is set)"]
316
+ end
317
+ elsif !seeded_value?(name, existing['data'])
318
+ # Says what is known - that this value didn't come from here - rather
319
+ # than claiming it was edited. An unrecorded setting is the normal case
320
+ # on a deployment upgrading into this feature: nobody changed it, it was
321
+ # written by seed_database or by a release before provenance existed.
322
+ [:skip, "Setting '#{name}' holds a value initsettings didn't write - leaving as " \
323
+ "#{existing['data'].inspect} (set #{OVERWRITE_ENV_VAR} to replace it)"]
324
+ elsif existing['data'] == value
325
+ [:skip, "Setting '#{name}' already matches #{value.inspect} - leaving unchanged"]
326
+ else
327
+ [:write, "Updating unedited setting '#{name}': #{existing['data'].inspect} -> #{value.inspect}"]
328
+ end
329
+ end
330
+
331
+ def self.redis_available?
332
+ names()
333
+ true
334
+ rescue StandardError
335
+ false
336
+ end
337
+
338
+ # Provenance for the values this seeder wrote, so a later init can tell an
339
+ # untouched setting from one an operator changed in the Admin Console.
340
+ # Without it the seeder has two bad options: never update (so editing the
341
+ # env var does nothing) or always update (so Admin Console edits silently
342
+ # revert on every restart).
343
+ SEEDED_PRIMARY_KEY = 'openc3__settings_seeded'
344
+
345
+ # @return [Boolean] true when the current value is the one we last seeded,
346
+ # meaning nobody has changed it since.
347
+ #
348
+ # An unrecorded setting is NOT treated as seeded. It was set by the Admin
349
+ # Console, by seed_database, or by a release before this tracking existed,
350
+ # and overwriting it is the silent clobber this mechanism exists to
351
+ # prevent. The cost is that a deployment upgrading into this feature needs
352
+ # one run with OVERWRITE_ENV_VAR before env changes take effect again. That
353
+ # cost is documented for operators too - compose.override.yaml under
354
+ # OPENC3_SETTINGS_OVERWRITE and `cli initsettings --help` both say it -
355
+ # so keep those in sync with this behavior.
356
+ def self.seeded_value?(name, current)
357
+ recorded = Store.hget(SEEDED_PRIMARY_KEY, name)
358
+ return false if recorded.nil?
359
+ JSON.parse(recorded, allow_nan: true, create_additions: true)['data'] == current
360
+ rescue JSON::ParserError
361
+ false
362
+ end
363
+
364
+ def self.record_seeded(name, value)
365
+ Store.hset(SEEDED_PRIMARY_KEY, name, JSON.generate({ 'data' => value.as_json(allow_nan: true) }))
366
+ end
367
+
368
+ # Collect every OPENC3_SETTING_<NAME> variable into a name => value hash.
369
+ #
370
+ # @param env [Hash] environment to read from
371
+ # @param problems [Array<String>] collects a value that can't be coerced, so
372
+ # OPENC3_SETTING_AI_CHAT=nope is reported and skipped alongside every other
373
+ # bad value rather than escaping as an exception and aborting init
374
+ # @return [Hash] setting name => coerced value
375
+ def self.parse_defaults_env(env, problems = [])
376
+ settings = {}
377
+ env.each do |key, value|
378
+ key = to_str(key)
379
+ next unless key.start_with?(SETTING_ENV_PREFIX)
380
+ name = key[SETTING_ENV_PREFIX.length..-1].downcase
381
+ next if name.empty?
382
+ begin
383
+ settings[name] = coerce(name, to_str(value))
384
+ rescue StandardError => error
385
+ problems << error.message
386
+ end
387
+ end
388
+ settings
389
+ end
390
+
391
+ # Environment variables and local mode files are always strings, but a
392
+ # setting holds whatever the Admin Console stores - a boolean for 'ai_chat',
393
+ # a String for everything else, including the settings whose String happens
394
+ # to contain JSON.
395
+ #
396
+ # Driven by the declared type rather than by attempting JSON.parse on
397
+ # everything: parse-and-see turns 'classification_banner' into a Hash and
398
+ # the component's own JSON.parse then throws on it, and it would turn a
399
+ # subtitle of "2024" into a number.
400
+ #
401
+ # @param name [String] setting name
402
+ # @param value [String] raw value
403
+ # @return [Object] value in the form the frontend expects
404
+ def self.coerce(name, value)
405
+ return value unless value.is_a?(String)
406
+ case KNOWN_SETTINGS.dig(name, :type)
407
+ when :boolean
408
+ ConfigParser.handle_true_false_strict(value, description: "setting '#{name}'")
409
+ when :json
410
+ # Parsed, because this setting's reader checks for an object and
411
+ # discards text. Validated here so a malformed blob is reported at seed
412
+ # time rather than read back as a default nobody asked for.
413
+ parse_json!(name, value)
414
+ when :json_text
415
+ # Kept as text, but parsed anyway to prove it is valid - the component
416
+ # that JSON.parses it has no way to report a failure
417
+ parse_json!(name, value)
418
+ value
419
+ else
420
+ value
421
+ end
422
+ end
423
+
424
+ # @return [Object] the parsed blob
425
+ # @raise [RuntimeError] when the value isn't a JSON object with the keys the
426
+ # setting's reader requires
427
+ def self.parse_json!(name, value)
428
+ parsed = JSON.parse(value)
429
+ unless parsed.is_a?(Hash)
430
+ raise "Value for setting '#{name}' must be a JSON object, got #{parsed.class}"
431
+ end
432
+ required = KNOWN_SETTINGS.dig(name, :require_keys)
433
+ if required
434
+ missing = required - parsed.keys
435
+ unless missing.empty?
436
+ raise "Value for setting '#{name}' is missing required key(s): #{missing.join(', ')}. " \
437
+ "Seed the whole object - a partial one breaks the code that reads it"
438
+ end
439
+ end
440
+ parsed
441
+ rescue JSON::ParserError => error
442
+ raise "Value for setting '#{name}' is not valid JSON: #{error.message}"
443
+ end
444
+
445
+ # Largest value we will write. Settings are read into every browser tab, so
446
+ # this is a guard against an env var that is a file by mistake rather than a
447
+ # real limit - the biggest real setting is a few hundred bytes.
448
+ MAX_VALUE_BYTES = 64 * 1024
449
+
450
+ # Raise on anything we can prove is wrong, so the operator is told rather
451
+ # than left with a tool silently falling back to its built-in default.
452
+ #
453
+ # Raising here does NOT abort init: apply_defaults collects the message,
454
+ # skips that one setting and keeps going, because crash-looping the init
455
+ # container over a cosmetic setting would be worse than using the default.
456
+ # `initsettings --dry-run` is the mode that exits non-zero.
457
+ def self.validate_setting!(name, value, allow_unknown: false)
458
+ unless name =~ /\A[a-z0-9_]+\z/
459
+ raise "Invalid setting name #{name.inspect}. Names must be lowercase letters, numbers and underscores"
460
+ end
461
+
462
+ unless KNOWN_SETTINGS.key?(name)
463
+ unless allow_unknown
464
+ # Period here, not on the suggestion - without a near match the
465
+ # sentence used to run straight into "Set OPENC3_SETTINGS_ALLOW_UNKNOWN"
466
+ message = "'#{name}' is not a known COSMOS setting."
467
+ suggestion = KNOWN_SETTINGS.keys.find { |known| near_match?(known, name) }
468
+ message += " Did you mean '#{suggestion}'?" if suggestion
469
+ message += " Set #{ALLOW_UNKNOWN_ENV_VAR} to apply it anyway."
470
+ raise message
471
+ end
472
+ puts "WARNING: '#{name}' is not a known COSMOS setting - #{ALLOW_UNKNOWN_ENV_VAR} is set, applying anyway"
473
+ end
474
+
475
+ size = value.is_a?(String) ? value.bytesize : JSON.generate(value).bytesize
476
+ if size > MAX_VALUE_BYTES
477
+ raise "Value for setting '#{name}' is #{size} bytes, exceeds the #{MAX_VALUE_BYTES} byte limit"
478
+ end
479
+
480
+ allowed = KNOWN_SETTINGS.dig(name, :values)
481
+ return if allowed.nil? or allowed.include?(value)
482
+ # Report the coerced value so 'Local' vs 'local' is visible in the error
483
+ raise "Invalid value #{value.inspect} for setting '#{name}'. Must be one of: #{allowed.map(&:inspect).join(', ')}"
484
+ end
485
+
486
+ # The stored settings as paste-ready OPENC3_SETTING_* lines, for
487
+ # `cli initsettings --export`.
488
+ #
489
+ # This is the answer to "what do I put in a JSON setting" - configure it in
490
+ # the Admin Console, run --export, paste the line. Reading the shape out of
491
+ # a running system can't drift from the code the way a documented example
492
+ # can, and it captures the values an operator already tuned by hand.
493
+ #
494
+ # @param env_only [Boolean] skip settings that have no value stored
495
+ # @return [Array<String>] one compose "environment:" list item per setting
496
+ def self.export_lines
497
+ KNOWN_SETTINGS.keys.filter_map do |name|
498
+ setting = get(name: name)
499
+ next if setting.nil?
500
+ "- #{yaml_env_item(name, setting['data'])}"
501
+ end
502
+ end
503
+
504
+ # One "KEY=value" env entry, quoted when YAML would otherwise mangle it.
505
+ # JSON always contains '":', which YAML reads as a mapping, so those always
506
+ # need quoting - the exact trap the compose.override.yaml comments warn about.
507
+ def self.yaml_env_item(name, data)
508
+ value = data.is_a?(String) ? data : JSON.generate(data)
509
+ entry = "#{SETTING_ENV_PREFIX}#{name.upcase}=#{value}"
510
+ return entry unless entry =~ /[:#"'\\]|\A\s|\s\z/
511
+ %("#{entry.gsub('\\', '\\\\\\\\').gsub('"', '\\\\"')}")
512
+ end
513
+
514
+ # Every setting that can be seeded, with its allowed values, for the
515
+ # `cli initsettings --help` listing.
516
+ #
517
+ # @return [Array<String>] one 'name: allowed values' line per setting
518
+ def self.describe_settings
519
+ KNOWN_SETTINGS.map do |name, details|
520
+ allowed = if details[:values]
521
+ details[:values].join(', ')
522
+ else
523
+ case details[:type]
524
+ when :boolean then 'true, false (1, 0 also work)'
525
+ when :json, :json_text
526
+ keys = details[:require_keys]
527
+ keys ? "JSON object with keys: #{keys.join(', ')}" : 'JSON object'
528
+ else 'any text'
529
+ end
530
+ end
531
+ "#{name}: #{allowed}"
532
+ end
533
+ end
534
+
535
+ # The JSON settings with a valid example value, for `--help`. Kept separate
536
+ # from describe_settings because a blob is far too long for that one-line
537
+ # "name: allowed values" format.
538
+ #
539
+ # @return [Array<Array(String, String)>] name and example, in table order
540
+ def self.describe_json_settings
541
+ KNOWN_SETTINGS.filter_map do |name, details|
542
+ next unless [:json, :json_text].include?(details[:type])
543
+ [name, details[:example]]
544
+ end
545
+ end
546
+
547
+ # Cheap typo detection: same length with one character different, or one
548
+ # character inserted / deleted. Enough to catch 'time_zones' and 'timezone'
549
+ # without pulling in a Levenshtein dependency.
550
+ def self.near_match?(known, name)
551
+ return false if (known.length - name.length).abs > 1
552
+ long, short = known.length >= name.length ? [known, name] : [name, known]
553
+ i = 0
554
+ i += 1 while i < short.length and long[i] == short[i]
555
+ return true if i == short.length and long.length - short.length <= 1
556
+ if long.length == short.length
557
+ long[(i + 1)..-1] == short[(i + 1)..-1]
558
+ else
559
+ long[(i + 1)..-1] == short[i..-1]
560
+ end
561
+ end
562
+
563
+ # Read a boolean control variable. Unset means false.
564
+ #
565
+ # The OPENC3_NO_* install flags are enabled by presence, so 'VAR=0' turns
566
+ # them ON. That is fine for "skip installing a tool" and wrong here:
567
+ # OVERWRITE_ENV_VAR discards what an operator configured in the Admin
568
+ # Console, so '0' and 'false' have to mean off, as they read.
569
+ #
570
+ # @param env [Hash] environment to read from
571
+ # @param name [String] variable name
572
+ # @return [Boolean]
573
+ def self.truthy_env?(env, name)
574
+ ConfigParser.handle_true_false_strict(to_str(env[name]), description: name)
575
+ end
576
+
577
+ # ENV values are frozen Strings but a Hash passed in tests may hold symbols
578
+ def self.to_str(value)
579
+ value.nil? ? nil : value.to_s
580
+ end
581
+
39
582
  def initialize(name:, scope: nil, data:)
40
583
  super(PRIMARY_KEY, name: name, scope: scope)
41
584
  @data = data
File without changes
File without changes
@@ -106,20 +106,17 @@ module OpenC3
106
106
  targets = self.all(scope: scope)
107
107
  targets.each { |_target_name, target| target['modified'] = false }
108
108
 
109
+ modified_targets = []
109
110
  if ENV['OPENC3_LOCAL_MODE']
110
- modified_targets = OpenC3::LocalMode.modified_targets(scope: scope)
111
- modified_targets.each do |target_name|
112
- targets[target_name]['modified'] = true if targets[target_name]
113
- end
114
- else
115
- modified_targets = Bucket.getClient().list_files(bucket: ENV['OPENC3_CONFIG_BUCKET'], path: "DEFAULT/targets_modified/", only_directories: true)
116
- modified_targets.each do |target_name|
117
- # A target could have been deleted without removing the modified files
118
- # Thus we have to check for the existence of the target_name key
119
- if targets.has_key?(target_name)
120
- targets[target_name]['modified'] = true
121
- end
122
- end
111
+ modified_targets += OpenC3::LocalMode.modified_targets(scope: scope)
112
+ end
113
+ # Always list the bucket, even in local mode, since the local directory
114
+ # only holds what COSMOS itself wrote there
115
+ modified_targets += Bucket.getClient().list_files(bucket: ENV['OPENC3_CONFIG_BUCKET'], path: "#{scope}/targets_modified/", only_directories: true)
116
+ modified_targets.each do |target_name|
117
+ # A target could have been deleted without removing the modified files
118
+ # Thus we have to check for the existence of the target_name key
119
+ targets[target_name]['modified'] = true if targets[target_name]
123
120
  end
124
121
  # Sort (which turns hash to array) and return hash
125
122
  # This enables a consistent listing of the targets
@@ -184,25 +181,33 @@ module OpenC3
184
181
  end
185
182
 
186
183
  # Given target's modified file list
184
+ # Returns the modified files as names relative to the scope,
185
+ # i.e. "INST/procedures/new.rb"
187
186
  def self.modified_files(target_name, scope:)
188
187
  modified = []
189
188
 
190
189
  if ENV['OPENC3_LOCAL_MODE']
191
- modified = OpenC3::LocalMode.modified_files(target_name, scope: scope)
192
- else
193
- resp = Bucket.getClient().list_objects(
194
- bucket: ENV['OPENC3_CONFIG_BUCKET'],
195
- # The trailing slash is important!
196
- prefix: "#{scope}/targets_modified/#{target_name}/",
197
- )
198
- resp.each do |item|
199
- # Results look like DEFAULT/targets_modified/INST/procedures/new.rb
200
- # so split on '/' and ignore the first two values
201
- modified << item.key.split('/')[2..-1].join('/')
190
+ # LocalMode reports target relative paths, i.e. "procedures/new.rb",
191
+ # so add the target name to match the bucket listing below
192
+ modified += OpenC3::LocalMode.modified_files(target_name, scope: scope).map do |file_path|
193
+ "#{target_name}/#{file_path}"
202
194
  end
203
195
  end
196
+ # Always list the bucket, even in local mode. The local directory only
197
+ # holds what COSMOS itself wrote there, so a file placed directly in the
198
+ # bucket is modified but has no local copy.
199
+ resp = Bucket.getClient().list_objects(
200
+ bucket: ENV['OPENC3_CONFIG_BUCKET'],
201
+ # The trailing slash is important!
202
+ prefix: "#{scope}/targets_modified/#{target_name}/",
203
+ )
204
+ resp.each do |item|
205
+ # Results look like DEFAULT/targets_modified/INST/procedures/new.rb
206
+ # so split on '/' and ignore the first two values
207
+ modified << item.key.split('/')[2..-1].join('/')
208
+ end
204
209
  # Sort to enable a consistent listing of the modified files
205
- modified.sort
210
+ modified.uniq.sort
206
211
  end
207
212
 
208
213
  # files: optional list of specific modified files to delete, each a name
File without changes
File without changes
@@ -16,6 +16,7 @@
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
18
  require 'openc3/models/model'
19
+ require 'openc3/topics/config_topic'
19
20
  # require 'openc3/models/scope_model' # Circular require
20
21
  require 'openc3/utilities/bucket'
21
22
  require 'openc3/utilities/bucket_utilities'
File without changes