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
@@ -15,27 +15,572 @@
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 written when it does not exist yet, or when it
186
+ # still holds the value last seeded (see seeded_value?), so a changed env
187
+ # var takes effect but a value changed in the Admin Console survives a
188
+ # container restart. Set OPENC3_SETTINGS_OVERWRITE to write on every run
189
+ # instead.
190
+ #
191
+ # Nothing here aborts init by default. A bad setting name, a bad value, or a
192
+ # malformed control variable is reported on stdout and that one setting is
193
+ # skipped, leaving COSMOS on its built-in default. Set STRICT_ENV_VAR to
194
+ # fail init instead; --dry-run always fails.
195
+ #
196
+ # @param env [Hash] environment to read from, defaults to ENV
197
+ # @param overwrite [Boolean, nil] nil reads OVERWRITE_ENV_VAR from env
198
+ # @param strict [Boolean, nil] nil reads STRICT_ENV_VAR from env
199
+ # @param dry_run [Boolean] report what would happen and write nothing.
200
+ # Reports every problem rather than aborting on the first, and works
201
+ # before Redis is up so it can be run ahead of starting COSMOS.
202
+ # @return [Array<String>] names of the settings that were written
203
+ def self.apply_defaults(env: ENV, overwrite: nil, dry_run: false, strict: nil)
204
+ problems = []
205
+ strict = read_control_flag(env, STRICT_ENV_VAR, problems) if strict.nil?
206
+ overwrite = read_control_flag(env, OVERWRITE_ENV_VAR, problems) if overwrite.nil?
207
+ allow_unknown = read_control_flag(env, ALLOW_UNKNOWN_ENV_VAR, problems)
208
+ # Collects a value that can't be coerced, so OPENC3_SETTING_AI_CHAT=nope is
209
+ # reported like any other bad value rather than escaping as an exception
210
+ settings = parse_defaults_env(env, problems)
211
+
212
+ prefix = dry_run ? '[dry run] ' : ''
213
+ written = []
214
+ if settings.empty?
215
+ # Only when nothing matched the prefix. If every matching variable failed
216
+ # to coerce, settings is empty too, and claiming none were set is a lie -
217
+ # the errors reported below are the explanation
218
+ if problems.empty?
219
+ puts "#{prefix}No #{SETTING_ENV_PREFIX}* environment variables set - nothing to seed"
220
+ end
221
+ else
222
+ # A dry run is most useful before `openc3.sh start`, when there is no
223
+ # Redis to compare against. Names and values can still be checked.
224
+ comparable = dry_run ? redis_available? : true
225
+ puts "#{prefix}Redis is not reachable - checking names and values only" unless comparable
226
+
227
+ settings.each do |name, value|
228
+ begin
229
+ validate_setting!(name, value, allow_unknown: allow_unknown)
230
+ rescue StandardError => error
231
+ # Collect rather than abort. The init container restarts on failure
232
+ # (compose restart: on-failure, Kubernetes restartPolicy OnFailure),
233
+ # so raising here puts COSMOS in a crash loop over a cosmetic
234
+ # setting, with the cause buried in restarting container logs.
235
+ # Skipping leaves the setting at its default, which is the same
236
+ # outcome as not setting it, and the error is reported below.
237
+ problems << error.message
238
+ next
239
+ end
240
+
241
+ existing = comparable ? get(name: name) : nil
242
+ action, message = plan_setting(name, value, existing, overwrite)
243
+ message += ' (current value unknown)' unless comparable
244
+ puts "#{prefix}#{message}"
245
+ next if action == :skip
246
+
247
+ # :record leaves the setting alone and only refreshes provenance, so it
248
+ # is not reported as written
249
+ written << name if action == :write
250
+ next if dry_run
251
+ set({ name: name, data: value }, scope: nil) if action == :write
252
+ record_seeded(name, value)
253
+ end
254
+ end
255
+
256
+ report_problems(problems, prefix: prefix, dry_run: dry_run, strict: strict)
257
+ written
258
+ end
259
+
260
+ # Print every problem and decide whether it should end the process.
261
+ #
262
+ # A typo in a cosmetic setting must not put the init container in a restart
263
+ # loop with the cause buried in restarting container logs, so the default is
264
+ # report-and-continue. Two things opt into failing: --dry-run, which exists
265
+ # to be a preflight gate, and STRICT_ENV_VAR, for a deployment that would
266
+ # rather not come up at all than come up misconfigured.
267
+ def self.report_problems(problems, prefix:, dry_run:, strict:)
268
+ return if problems.empty?
269
+ problems.each { |problem| puts "#{prefix}ERROR: #{problem}" }
270
+ # "problem" rather than "setting" - a malformed control variable is
271
+ # reported here too, and it isn't a setting that got skipped
272
+ summary = "#{problems.length} #{SETTING_ENV_PREFIX}* configuration problem(s)"
273
+ if dry_run or strict
274
+ puts "#{prefix}#{summary}"
275
+ else
276
+ puts "#{prefix}#{summary} - the affected setting(s) were skipped and COSMOS will use the default"
277
+ puts "#{prefix}Set #{STRICT_ENV_VAR} to fail init on these instead of continuing"
278
+ end
279
+ $stdout.flush
280
+ raise "#{summary}: #{problems.join('; ')}" if dry_run or strict
281
+ end
282
+
283
+ # Read a boolean control variable, reporting an unparsable value rather than
284
+ # letting it abort init. Off is the safe reading of all three: OVERWRITE off
285
+ # doesn't discard an Admin Console edit, ALLOW_UNKNOWN off doesn't write a
286
+ # name nothing reads, and STRICT off doesn't fail init.
287
+ #
288
+ # @param problems [Array<String>] collects the message when the value is bad
289
+ # @return [Boolean]
290
+ def self.read_control_flag(env, name, problems)
291
+ truthy_env?(env, name)
292
+ rescue StandardError => error
293
+ problems << "#{error.message} - treating #{name} as off"
294
+ false
295
+ end
296
+
297
+ # What apply_defaults will do with one setting. Single-sourced so a dry run
298
+ # cannot report one thing and the real run do another.
299
+ #
300
+ # @return [Array(Symbol, String)] :write, :record or :skip, and the line to
301
+ # log. :record means the stored value is already correct but provenance
302
+ # still needs writing, so the setting itself is left untouched.
303
+ def self.plan_setting(name, value, existing, overwrite)
304
+ if existing.nil?
305
+ [:write, "Set default setting '#{name}' to: #{value.inspect}"]
306
+ elsif overwrite
307
+ if existing['data'] == value
308
+ # Rewriting the same value would bump updated_at on every init and
309
+ # report a write that changed nothing. Provenance is still recorded,
310
+ # so the next run without OVERWRITE can tell this value came from the
311
+ # environment rather than from an Admin Console edit.
312
+ [:record, "Setting '#{name}' already matches #{value.inspect}"]
313
+ else
314
+ # Overwrite discards an Admin Console edit, so say what was lost -
315
+ # otherwise the log reads identically to a first-time seed
316
+ [:write, "Overwriting setting '#{name}': #{existing['data'].inspect} -> " \
317
+ "#{value.inspect} (#{OVERWRITE_ENV_VAR} is set)"]
318
+ end
319
+ elsif !seeded_value?(name, existing['data'])
320
+ # Says what is known - that this value didn't come from here - rather
321
+ # than claiming it was edited. An unrecorded setting is the normal case
322
+ # on a deployment upgrading into this feature: nobody changed it, it was
323
+ # written by seed_database or by a release before provenance existed.
324
+ [:skip, "Setting '#{name}' holds a value initsettings didn't write - leaving as " \
325
+ "#{existing['data'].inspect} (set #{OVERWRITE_ENV_VAR} to replace it)"]
326
+ elsif existing['data'] == value
327
+ [:skip, "Setting '#{name}' already matches #{value.inspect} - leaving unchanged"]
328
+ else
329
+ [:write, "Updating unedited setting '#{name}': #{existing['data'].inspect} -> #{value.inspect}"]
330
+ end
331
+ end
332
+
333
+ def self.redis_available?
334
+ names()
335
+ true
336
+ rescue StandardError
337
+ false
338
+ end
339
+
340
+ # Provenance for the values this seeder wrote, so a later init can tell an
341
+ # untouched setting from one an operator changed in the Admin Console.
342
+ # Without it the seeder has two bad options: never update (so editing the
343
+ # env var does nothing) or always update (so Admin Console edits silently
344
+ # revert on every restart).
345
+ SEEDED_PRIMARY_KEY = 'openc3__settings_seeded'
346
+
347
+ # @return [Boolean] true when the current value is the one we last seeded,
348
+ # meaning nobody has changed it since.
349
+ #
350
+ # An unrecorded setting is NOT treated as seeded. It was set by the Admin
351
+ # Console, by seed_database, or by a release before this tracking existed,
352
+ # and overwriting it is the silent clobber this mechanism exists to
353
+ # prevent. The cost is that a deployment upgrading into this feature needs
354
+ # one run with OVERWRITE_ENV_VAR before env changes take effect again. That
355
+ # cost is documented for operators too - compose.override.yaml under
356
+ # OPENC3_SETTINGS_OVERWRITE and `cli initsettings --help` both say it -
357
+ # so keep those in sync with this behavior.
358
+ def self.seeded_value?(name, current)
359
+ recorded = Store.hget(SEEDED_PRIMARY_KEY, name)
360
+ return false if recorded.nil?
361
+ JSON.parse(recorded, allow_nan: true, create_additions: true)['data'] == current
362
+ rescue JSON::ParserError
363
+ false
364
+ end
365
+
366
+ def self.record_seeded(name, value)
367
+ Store.hset(SEEDED_PRIMARY_KEY, name, JSON.generate({ 'data' => value.as_json(allow_nan: true) }))
368
+ end
369
+
370
+ # Collect every OPENC3_SETTING_<NAME> variable into a name => value hash.
371
+ #
372
+ # @param env [Hash] environment to read from
373
+ # @param problems [Array<String>] collects a value that can't be coerced, so
374
+ # OPENC3_SETTING_AI_CHAT=nope is reported and skipped alongside every other
375
+ # bad value rather than escaping as an exception and aborting init
376
+ # @return [Hash] setting name => coerced value
377
+ def self.parse_defaults_env(env, problems = [])
378
+ settings = {}
379
+ env.each do |key, value|
380
+ key = to_str(key)
381
+ next unless key.start_with?(SETTING_ENV_PREFIX)
382
+ name = key[SETTING_ENV_PREFIX.length..-1].downcase
383
+ next if name.empty?
384
+ begin
385
+ settings[name] = coerce(name, to_str(value))
386
+ rescue StandardError => error
387
+ problems << error.message
388
+ end
389
+ end
390
+ settings
391
+ end
392
+
393
+ # Environment variables and local mode files are always strings, but a
394
+ # setting holds whatever the Admin Console stores - a boolean for 'ai_chat',
395
+ # a String for everything else, including the settings whose String happens
396
+ # to contain JSON.
397
+ #
398
+ # Driven by the declared type rather than by attempting JSON.parse on
399
+ # everything: parse-and-see turns 'classification_banner' into a Hash and
400
+ # the component's own JSON.parse then throws on it, and it would turn a
401
+ # subtitle of "2024" into a number.
402
+ #
403
+ # @param name [String] setting name
404
+ # @param value [String] raw value
405
+ # @return [Object] value in the form the frontend expects
406
+ def self.coerce(name, value)
407
+ return value unless value.is_a?(String)
408
+ case KNOWN_SETTINGS.dig(name, :type)
409
+ when :boolean
410
+ ConfigParser.handle_true_false_strict(value, description: "setting '#{name}'")
411
+ when :json
412
+ # Parsed, because this setting's reader checks for an object and
413
+ # discards text. Validated here so a malformed blob is reported at seed
414
+ # time rather than read back as a default nobody asked for.
415
+ parse_json!(name, value)
416
+ when :json_text
417
+ # Kept as text, but parsed anyway to prove it is valid - the component
418
+ # that JSON.parses it has no way to report a failure
419
+ parse_json!(name, value)
420
+ value
421
+ else
422
+ value
423
+ end
424
+ end
425
+
426
+ # @return [Object] the parsed blob
427
+ # @raise [RuntimeError] when the value isn't a JSON object with the keys the
428
+ # setting's reader requires
429
+ def self.parse_json!(name, value)
430
+ parsed = JSON.parse(value)
431
+ unless parsed.is_a?(Hash)
432
+ raise "Value for setting '#{name}' must be a JSON object, got #{parsed.class}"
433
+ end
434
+ required = KNOWN_SETTINGS.dig(name, :require_keys)
435
+ if required
436
+ missing = required - parsed.keys
437
+ unless missing.empty?
438
+ raise "Value for setting '#{name}' is missing required key(s): #{missing.join(', ')}. " \
439
+ "Seed the whole object - a partial one breaks the code that reads it"
440
+ end
441
+ end
442
+ parsed
443
+ rescue JSON::ParserError => error
444
+ raise "Value for setting '#{name}' is not valid JSON: #{error.message}"
445
+ end
446
+
447
+ # Largest value we will write. Settings are read into every browser tab, so
448
+ # this is a guard against an env var that is a file by mistake rather than a
449
+ # real limit - the biggest real setting is a few hundred bytes.
450
+ MAX_VALUE_BYTES = 64 * 1024
451
+
452
+ # Raise on anything we can prove is wrong, so the operator is told rather
453
+ # than left with a tool silently falling back to its built-in default.
454
+ #
455
+ # Raising here does NOT abort init: apply_defaults collects the message,
456
+ # skips that one setting and keeps going, because crash-looping the init
457
+ # container over a cosmetic setting would be worse than using the default.
458
+ # `initsettings --dry-run` is the mode that exits non-zero.
459
+ def self.validate_setting!(name, value, allow_unknown: false)
460
+ unless name =~ /\A[a-z0-9_]+\z/
461
+ raise "Invalid setting name #{name.inspect}. Names must be lowercase letters, numbers and underscores"
462
+ end
463
+
464
+ unless KNOWN_SETTINGS.key?(name)
465
+ unless allow_unknown
466
+ # Period here, not on the suggestion - without a near match the
467
+ # sentence used to run straight into "Set OPENC3_SETTINGS_ALLOW_UNKNOWN"
468
+ message = "'#{name}' is not a known COSMOS setting."
469
+ suggestion = KNOWN_SETTINGS.keys.find { |known| near_match?(known, name) }
470
+ message += " Did you mean '#{suggestion}'?" if suggestion
471
+ message += " Set #{ALLOW_UNKNOWN_ENV_VAR} to apply it anyway."
472
+ raise message
473
+ end
474
+ puts "WARNING: '#{name}' is not a known COSMOS setting - #{ALLOW_UNKNOWN_ENV_VAR} is set, applying anyway"
475
+ end
476
+
477
+ size = value.is_a?(String) ? value.bytesize : JSON.generate(value).bytesize
478
+ if size > MAX_VALUE_BYTES
479
+ raise "Value for setting '#{name}' is #{size} bytes, exceeds the #{MAX_VALUE_BYTES} byte limit"
480
+ end
481
+
482
+ allowed = KNOWN_SETTINGS.dig(name, :values)
483
+ return if allowed.nil? or allowed.include?(value)
484
+ # Report the coerced value so 'Local' vs 'local' is visible in the error
485
+ raise "Invalid value #{value.inspect} for setting '#{name}'. Must be one of: #{allowed.map(&:inspect).join(', ')}"
486
+ end
487
+
488
+ # The stored settings as paste-ready OPENC3_SETTING_* lines, for
489
+ # `cli initsettings --export`.
490
+ #
491
+ # This is the answer to "what do I put in a JSON setting" - configure it in
492
+ # the Admin Console, run --export, paste the line. Reading the shape out of
493
+ # a running system can't drift from the code the way a documented example
494
+ # can, and it captures the values an operator already tuned by hand.
495
+ #
496
+ # @param env_only [Boolean] skip settings that have no value stored
497
+ # @return [Array<String>] one compose "environment:" list item per setting
498
+ def self.export_lines
499
+ KNOWN_SETTINGS.keys.filter_map do |name|
500
+ setting = get(name: name)
501
+ next if setting.nil?
502
+ "- #{yaml_env_item(name, setting['data'])}"
503
+ end
504
+ end
505
+
506
+ # One "KEY=value" env entry, quoted when YAML would otherwise mangle it.
507
+ # JSON always contains '":', which YAML reads as a mapping, so those always
508
+ # need quoting - the exact trap the compose.override.yaml comments warn about.
509
+ def self.yaml_env_item(name, data)
510
+ value = data.is_a?(String) ? data : JSON.generate(data)
511
+ entry = "#{SETTING_ENV_PREFIX}#{name.upcase}=#{value}"
512
+ return entry unless entry =~ /[:#"'\\]|\A\s|\s\z/
513
+ %("#{entry.gsub('\\', '\\\\\\\\').gsub('"', '\\\\"')}")
514
+ end
515
+
516
+ # Every setting that can be seeded, with its allowed values, for the
517
+ # `cli initsettings --help` listing.
518
+ #
519
+ # @return [Array<String>] one 'name: allowed values' line per setting
520
+ def self.describe_settings
521
+ KNOWN_SETTINGS.map do |name, details|
522
+ allowed = if details[:values]
523
+ details[:values].join(', ')
524
+ else
525
+ case details[:type]
526
+ when :boolean then 'true, false (1, 0 also work)'
527
+ when :json, :json_text
528
+ keys = details[:require_keys]
529
+ keys ? "JSON object with keys: #{keys.join(', ')}" : 'JSON object'
530
+ else 'any text'
531
+ end
532
+ end
533
+ "#{name}: #{allowed}"
534
+ end
535
+ end
536
+
537
+ # The JSON settings with a valid example value, for `--help`. Kept separate
538
+ # from describe_settings because a blob is far too long for that one-line
539
+ # "name: allowed values" format.
540
+ #
541
+ # @return [Array<Array(String, String)>] name and example, in table order
542
+ def self.describe_json_settings
543
+ KNOWN_SETTINGS.filter_map do |name, details|
544
+ next unless [:json, :json_text].include?(details[:type])
545
+ [name, details[:example]]
546
+ end
547
+ end
548
+
549
+ # Cheap typo detection: same length with one character different, or one
550
+ # character inserted / deleted. Enough to catch 'time_zones' and 'timezone'
551
+ # without pulling in a Levenshtein dependency.
552
+ def self.near_match?(known, name)
553
+ return false if (known.length - name.length).abs > 1
554
+ long, short = known.length >= name.length ? [known, name] : [name, known]
555
+ i = 0
556
+ i += 1 while i < short.length and long[i] == short[i]
557
+ return true if i == short.length and long.length - short.length <= 1
558
+ if long.length == short.length
559
+ long[(i + 1)..-1] == short[(i + 1)..-1]
560
+ else
561
+ long[(i + 1)..-1] == short[i..-1]
562
+ end
563
+ end
564
+
565
+ # Read a boolean control variable. Unset means false.
566
+ #
567
+ # The OPENC3_NO_* install flags are enabled by presence, so 'VAR=0' turns
568
+ # them ON. That is fine for "skip installing a tool" and wrong here:
569
+ # OVERWRITE_ENV_VAR discards what an operator configured in the Admin
570
+ # Console, so '0' and 'false' have to mean off, as they read.
571
+ #
572
+ # @param env [Hash] environment to read from
573
+ # @param name [String] variable name
574
+ # @return [Boolean]
575
+ def self.truthy_env?(env, name)
576
+ ConfigParser.handle_true_false_strict(to_str(env[name]), description: name)
577
+ end
578
+
579
+ # ENV values are frozen Strings but a Hash passed in tests may hold symbols
580
+ def self.to_str(value)
581
+ value.nil? ? nil : value.to_s
582
+ end
583
+
39
584
  def initialize(name:, scope: nil, data:)
40
585
  super(PRIMARY_KEY, name: name, scope: scope)
41
586
  @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